{
  "openapi": "3.1.0",
  "info": {
    "title": "The Vesey Agent Gateway API",
    "version": "v1",
    "summary": "Public availability, guide prices and payment-first booking, built for AI agents.",
    "description": "Machine-readable booking surface for The Vesey Private Hospital (Sutton Coldfield, UK).\n\nThis API is designed for AI assistants: search services, read live guide prices, check real availability, and complete a booking end-to-end.\n\nPAYMENT-FIRST BOOKING: POST /api/agent/bookings never confirms a slot directly. It returns a Stripe-hosted checkout URL and a bookingIntentId; the appointment is confirmed only after the patient completes payment. Agents must hand the checkoutUrl to the patient (or the patient's authorised human) to finish.\n\nGUIDE PRICES ONLY: every price returned is a guide price in GBP, confirmed at booking. Prices may vary with clinical need. Nothing here is a firm quote or an offer to treat.\n\nFAIL CLOSED: the whole surface sits behind the server flag AGENT_API_ENABLED. When it is unset the endpoints return 404 (the API does not exist). Availability reflects real slots only — never fabricated urgency.\n\nRATE LIMITS: unauthenticated requests are rate limited at the edge (guideline: 60 requests/minute per IP for GET, 10/minute for POST /api/agent/bookings). Partnership API keys raise these limits; request one at reception@thevesey.co.uk. Exceeding a limit returns 429 with a Retry-After header.\n\nRESPONSIBLE USE / TERMS: no scraping of personal data, no medical advice, and bookings require the real patient's details and their consent. AGENT-SYNC: replace the terms URL below with the reviewed Terms of Service before go-live.",
    "termsOfService": "https://www.thevesey.co.uk/agent-api-terms",
    "x-terms-status": "AGENT-SYNC: placeholder — legal review required before publishing; confirm final ToS URL with the gateway workstream.",
    "contact": {
      "name": "The Vesey — AI partnerships",
      "email": "reception@thevesey.co.uk",
      "url": "https://www.thevesey.co.uk/agent-api"
    }
  },
  "servers": [
    { "url": "https://www.thevesey.co.uk", "description": "Production" }
  ],
  "x-mcp": {
    "endpoint": "https://www.thevesey.co.uk/api/mcp",
    "description": "The same capability is exposed as a remote MCP server. Tools: search_services, get_service, check_availability, request_booking, get_clinic_facts.",
    "x-agent-sync": "Tool names/args mirror the documented gateway contract; reconcile with the live /api/mcp implementation."
  },
  "tags": [
    { "name": "services", "description": "Service discovery and guide prices." },
    { "name": "availability", "description": "Live bookable slots." },
    { "name": "bookings", "description": "Payment-first booking intents." },
    { "name": "feeds", "description": "Versioned machine-readable feeds." }
  ],
  "paths": {
    "/api/agent/services": {
      "get": {
        "operationId": "searchServices",
        "summary": "Search services",
        "description": "Search the public catalogue and return matching services with their guide prices.",
        "tags": ["services"],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search (e.g. 'private gp', 'thyroid blood test').",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching services.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" } }
                  },
                  "required": ["services"]
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/agent/services/{id}": {
      "get": {
        "operationId": "getService",
        "summary": "Get one service",
        "description": "Return a single service by its stable id, including its guide price.",
        "tags": ["services"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Stable service id (as returned by searchServices or the pricing feed).",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The service.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Service" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/agent/availability": {
      "get": {
        "operationId": "checkAvailability",
        "summary": "Check availability",
        "description": "Return real bookable slots for a service over the next N days. Empty when there is genuinely no availability — never fabricated.",
        "tags": ["availability"],
        "parameters": [
          {
            "name": "service",
            "in": "query",
            "required": true,
            "description": "Service id to check availability for.",
            "schema": { "type": "string" }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "How many days ahead to search (default 7).",
            "schema": { "type": "integer", "minimum": 1, "maximum": 30, "default": 7 }
          }
        ],
        "responses": {
          "200": {
            "description": "Available slots.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": { "type": "string" },
                    "slots": { "type": "array", "items": { "$ref": "#/components/schemas/AvailabilitySlot" } }
                  },
                  "required": ["service", "slots"]
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/agent/bookings": {
      "post": {
        "operationId": "requestBooking",
        "summary": "Request a booking (payment-first)",
        "description": "Create a booking intent for a real patient. Returns a Stripe-hosted checkout URL; the appointment is confirmed only once the patient completes payment. The patient's real details and consent are required.",
        "tags": ["bookings"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingRequest" } } }
        },
        "responses": {
          "200": {
            "description": "A booking intent with a payment link.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/feeds/pricing": {
      "get": {
        "operationId": "getPricingFeed",
        "summary": "Versioned pricing feed",
        "description": "A cache-friendly (max-age 3600) versioned JSON feed of every public service and its guide price. Built from the same cleaned price index the clinic's AI channels use.",
        "tags": ["feeds"],
        "responses": {
          "200": {
            "description": "The pricing feed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PricingFeed" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "responses": {
      "BadRequest": {
        "description": "Invalid request.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Not found, or the agent API is disabled (fail-closed).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. Retry after the interval in the Retry-After header.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Service": {
        "type": "object",
        "description": "A bookable service with its guide price.",
        "properties": {
          "id": { "type": "string", "description": "Stable service id.", "examples": ["gp-appointment-30-min-90-170"] },
          "name": { "type": "string", "examples": ["GP appointment (30 min £90 / 40 min £170)"] },
          "category": { "type": "string", "examples": ["Clinic service", "Blood test", "GP subscription"] },
          "guidePrice": { "type": "number", "description": "Guide price in GBP, confirmed at booking.", "examples": [90] },
          "currency": { "type": "string", "const": "GBP" },
          "bookUrl": { "type": "string", "format": "uri", "description": "Human web booking deep link (agents book via POST /api/agent/bookings)." },
          "page": { "type": "string", "format": "uri", "description": "Human-facing information page." }
        },
        "required": ["id", "name", "category", "guidePrice"]
      },
      "AvailabilitySlot": {
        "type": "object",
        "description": "A single bookable slot.",
        "properties": {
          "service": { "type": "string", "description": "Service id this slot is for." },
          "start": { "type": "string", "format": "date-time", "description": "Slot start, ISO-8601 (Europe/London)." },
          "end": { "type": "string", "format": "date-time", "description": "Slot end, ISO-8601." },
          "label": { "type": "string", "examples": ["today 14:30", "Thu 18 Jul, 09:00"] },
          "available": { "type": "boolean", "default": true }
        },
        "required": ["service", "start"]
      },
      "BookingRequest": {
        "type": "object",
        "description": "A booking request for a real patient. All fields required.",
        "properties": {
          "name": { "type": "string", "description": "Patient full name." },
          "email": { "type": "string", "format": "email", "description": "Patient email (checkout + confirmation)." },
          "phone": { "type": "string", "description": "Patient phone (E.164 preferred)." },
          "service": { "type": "string", "description": "Service id to book." },
          "dateTime": { "type": "string", "format": "date-time", "description": "Requested slot start (from checkAvailability)." }
        },
        "required": ["name", "email", "phone", "service", "dateTime"]
      },
      "BookingResponse": {
        "type": "object",
        "description": "A payment-first booking intent. The slot is confirmed only after checkout completes.",
        "properties": {
          "checkoutUrl": { "type": "string", "format": "uri", "description": "Stripe-hosted checkout URL — hand this to the patient to confirm and pay." },
          "bookingIntentId": { "type": "string", "description": "Opaque id for this pending booking." },
          "guidePriceNotice": { "type": "string", "description": "Guide-price caveat to surface verbatim to the patient." },
          "status": { "type": "string", "enum": ["payment_required"], "default": "payment_required" },
          "expiresAt": { "type": "string", "format": "date-time", "description": "When the held intent / checkout URL expires." }
        },
        "required": ["checkoutUrl", "bookingIntentId", "guidePriceNotice"]
      },
      "PricingFeed": {
        "type": "object",
        "description": "Versioned pricing feed.",
        "properties": {
          "version": { "type": "string", "const": "v1" },
          "generatedAt": { "type": "string", "format": "date-time" },
          "currency": { "type": "string", "const": "GBP" },
          "guidePriceNotice": { "type": "string" },
          "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" } }
        },
        "required": ["version", "generatedAt", "currency", "guidePriceNotice", "services"]
      },
      "Error": {
        "type": "object",
        "description": "Standard error envelope.",
        "properties": {
          "error": { "type": "string", "description": "Human-readable error message." },
          "code": { "type": "string", "description": "Optional machine-readable error code." }
        },
        "required": ["error"]
      }
    }
  }
}
