{
  "openapi": "3.1.0",
  "info": {
    "title": "La Maison du Mile-End — public booking API",
    "version": "1.0.0",
    "summary": "Live availability and all-in pricing for a stay.",
    "description": "The one unauthenticated endpoint published by La Maison du Mile-End, a 7-room bed and breakfast at 204 Rue Bernard, Montreal, Quebec H2T 2K4.\n\nTotals are all-in: room, cleaning fee and Quebec lodging tax, GST and QST are already included. No API key is required.\n\n**Rate limit.** 40 requests per 60 seconds per client. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy`, so a caller can pace itself instead of discovering the limit by being refused; a 429 adds `Retry-After`.\n\n**Versioning.** The major version is in the path (`/api/v1`). Inside a major version only additive changes ship: new endpoints, new optional request parameters, new fields on an existing response object, new error codes on an existing status. Anything breaking — removing or renaming a response field, changing the type or meaning of a response field, making an optional request parameter required, removing an endpoint, changing an HTTP status for an existing outcome — ships as a new major version instead, and the old one keeps answering for at least 180 days carrying `Deprecation` (RFC 9745), `Sunset` (RFC 8594) and a `Link` with rel=\"successor-version\". The policy is published in full at https://www.maisonmileend.ca/deprecation-policy.md.\n\n**Errors** are JSON in a single envelope: `code`, `status`, `message`, an optional `hint` and `details`, plus `documentation_url`. See the Error schema.\n\nCompleting a reservation needs a payment method and a person: send the traveller to https://www.maisonmileend.ca/en/book, or call +1-514-700-3932. Agent instructions: https://www.maisonmileend.ca/agents.md.",
    "contact": {
      "name": "La Maison du Mile-End",
      "url": "https://www.maisonmileend.ca/en/agents",
      "email": "info@maisonmileend.ca"
    }
  },
  "servers": [
    {
      "url": "https://www.maisonmileend.ca",
      "description": "Production. The major version is carried in the path."
    }
  ],
  "paths": {
    "/api/v1/booking/availability": {
      "get": {
        "operationId": "searchAvailability",
        "summary": "Search availability and all-in pricing for a stay.",
        "description": "Returns the room types with at least one unit free on every night of the window and enough capacity for the whole party, plus multi-room combinations when the party does not fit in one room.\n\nA single room sleeps at most 2 guests; a party of up to 14 is seated across rooms.",
        "parameters": [
          {
            "name": "arrival",
            "in": "query",
            "required": true,
            "description": "First night of the stay.",
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2026-09-12"
          },
          {
            "name": "departure",
            "in": "query",
            "required": true,
            "description": "Departure date. Must be strictly after `arrival`.",
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "example": "2026-09-15"
          },
          {
            "name": "adults",
            "in": "query",
            "required": false,
            "description": "Adults in the party.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 14,
              "default": 2
            },
            "example": 2
          },
          {
            "name": "children",
            "in": "query",
            "required": false,
            "description": "Children in the party.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 14,
              "default": 0
            },
            "example": 0
          }
        ],
        "responses": {
          "200": {
            "description": "Availability for the requested window.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed per window.",
                "schema": {
                  "type": "integer",
                  "example": 40
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the window rolls over.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "40;w=60"
                }
              },
              "API-Version": {
                "description": "The major version that served this response.",
                "schema": {
                  "type": "string",
                  "example": "v1"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityResult"
                }
              }
            }
          },
          "400": {
            "description": "The query failed validation; `details` names the failing fields.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed per window.",
                "schema": {
                  "type": "integer",
                  "example": 40
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the window rolls over.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "40;w=60"
                }
              },
              "API-Version": {
                "description": "The major version that served this response.",
                "schema": {
                  "type": "string",
                  "example": "v1"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "More than 40 requests in 60 seconds from one client.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed per window.",
                "schema": {
                  "type": "integer",
                  "example": 40
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the window rolls over.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "40;w=60"
                }
              },
              "API-Version": {
                "description": "The major version that served this response.",
                "schema": {
                  "type": "string",
                  "example": "v1"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The availability engine could not answer.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed per window.",
                "schema": {
                  "type": "integer",
                  "example": 40
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the window rolls over.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "40;w=60"
                }
              },
              "API-Version": {
                "description": "The major version that served this response.",
                "schema": {
                  "type": "string",
                  "example": "v1"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The single envelope every error on this API uses.",
        "required": [
          "error",
          "code",
          "status",
          "message",
          "documentation_url"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Same value as `code`. Kept for clients written against the original shape."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable, stable, snake_case. Branch on this.",
            "enum": [
              "invalid_request",
              "not_found",
              "method_not_allowed",
              "rate_limited",
              "upstream_error"
            ]
          },
          "status": {
            "type": "integer",
            "description": "Repeats the HTTP status."
          },
          "message": {
            "type": "string",
            "description": "One sentence describing what went wrong."
          },
          "hint": {
            "type": "string",
            "description": "What the caller should do about it."
          },
          "details": {
            "description": "Structured detail, shape depends on `code`."
          },
          "documentation_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ValidationError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "const": "invalid_request"
              },
              "details": {
                "type": "object",
                "description": "Zod `flatten()` output: `formErrors` plus `fieldErrors` by field name.",
                "additionalProperties": true
              }
            }
          }
        ]
      },
      "TaxLines": {
        "type": "object",
        "description": "Tax already included in every `total`, itemised.",
        "required": [
          "lodging",
          "gst",
          "qst"
        ],
        "properties": {
          "lodging": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "gst": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "qst": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          }
        }
      },
      "NightRate": {
        "type": "object",
        "required": [
          "date",
          "price"
        ],
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "price": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          }
        }
      },
      "PlanOption": {
        "type": "object",
        "description": "A rate plan sellable for this room over this window.",
        "required": [
          "ratePlanId",
          "name",
          "roomSubtotal",
          "cleaningFee",
          "taxes",
          "total"
        ],
        "properties": {
          "ratePlanId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "paymentMode": {
            "type": "string"
          },
          "policyType": {
            "type": [
              "string",
              "null"
            ]
          },
          "policyText": {
            "type": "object",
            "description": "Cancellation policy prose by locale.",
            "properties": {
              "fr": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            }
          },
          "cancellationDeadline": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Free-cancellation deadline, or null when never refundable."
          },
          "roomSubtotal": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "cleaningFee": {
            "type": "number",
            "format": "double",
            "description": "Per stay, pre-tax. Never per night."
          },
          "taxes": {
            "$ref": "#/components/schemas/TaxLines"
          },
          "total": {
            "type": "number",
            "format": "double",
            "description": "All-in total for the stay under this plan."
          },
          "perNightFrom": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "depositAmount": {
            "type": "number",
            "format": "double",
            "description": "Due at booking under this plan."
          }
        }
      },
      "AvailableRoom": {
        "type": "object",
        "required": [
          "roomTypeId",
          "name",
          "nights",
          "total",
          "currency",
          "plans"
        ],
        "properties": {
          "roomTypeId": {
            "type": "string",
            "format": "uuid"
          },
          "beds24RoomId": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "nights": {
            "type": "integer",
            "minimum": 1
          },
          "fromPricePerNight": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "roomSubtotal": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "cleaningFee": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "taxes": {
            "$ref": "#/components/schemas/TaxLines"
          },
          "total": {
            "type": "number",
            "format": "double",
            "description": "All-in stay total for one unit of this room type."
          },
          "currency": {
            "type": "string",
            "example": "CAD"
          },
          "nightRates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NightRate"
            }
          },
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanOption"
            }
          },
          "maxOccupancy": {
            "type": "integer",
            "maximum": 2
          },
          "maxAdults": {
            "type": [
              "integer",
              "null"
            ]
          },
          "unitsAvailable": {
            "type": "integer",
            "minimum": 1,
            "description": "Units of this type free on EVERY night of the window."
          }
        }
      },
      "AllocationRoom": {
        "type": "object",
        "description": "One room of a multi-room combination, with its share of the party.",
        "required": [
          "roomTypeId",
          "name",
          "adults",
          "children",
          "total"
        ],
        "properties": {
          "roomTypeId": {
            "type": "string",
            "format": "uuid"
          },
          "beds24RoomId": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "ratePlanId": {
            "type": [
              "string",
              "null"
            ]
          },
          "adults": {
            "type": "integer"
          },
          "children": {
            "type": "integer"
          },
          "total": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "preTax": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          }
        }
      },
      "RoomCombination": {
        "type": "object",
        "description": "A multi-room way to seat the party, best first (fewest rooms, then cheapest).",
        "required": [
          "rooms",
          "total",
          "nights"
        ],
        "properties": {
          "rooms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AllocationRoom"
            }
          },
          "total": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "preTax": {
            "type": "number",
            "format": "double",
            "description": "CAD, rounded to cents."
          },
          "nights": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "AvailabilityResult": {
        "type": "object",
        "required": [
          "arrival",
          "departure",
          "nights",
          "adults",
          "children",
          "currency",
          "rooms",
          "combinations"
        ],
        "properties": {
          "arrival": {
            "type": "string",
            "format": "date"
          },
          "departure": {
            "type": "string",
            "format": "date"
          },
          "nights": {
            "type": "integer",
            "minimum": 1
          },
          "adults": {
            "type": "integer"
          },
          "children": {
            "type": "integer"
          },
          "currency": {
            "type": "string",
            "example": "CAD"
          },
          "rooms": {
            "type": "array",
            "description": "Room types one unit of which seats the whole party.",
            "items": {
              "$ref": "#/components/schemas/AvailableRoom"
            }
          },
          "combinations": {
            "type": "array",
            "description": "Multi-room stays that seat the party. Empty when the party cannot be seated.",
            "items": {
              "$ref": "#/components/schemas/RoomCombination"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Agent instructions, and the versioning and deprecation policy",
    "url": "https://www.maisonmileend.ca/agents.md"
  },
  "x-deprecation-policy": "https://www.maisonmileend.ca/deprecation-policy.md"
}
