{
  "openapi": "3.1.0",
  "info": {
    "title": "Onagio API",
    "version": "1.2.0",
    "summary": "Read-only API for a Parmigiano-Reggiano producer in Parma, Italy.",
    "description": "Onagio S.p.A. ages DOP Parmigiano-Reggiano in a former Olivetti typewriter factory on Via Olivetti 42 in Parma. This API exposes the three wheels we make, their current stock and lead times, suggested pairings, the typewriter collection in the museum, and the factory tour schedule. It is read-only, requires no authentication, and imposes no rate limit beyond ordinary politeness. Orders are not placed through the API; email wholesale@onagio.com.",
    "contact": {
      "name": "Onagio API support",
      "email": "api@onagio.com",
      "url": "https://onagio.com/developers"
    },
    "license": { "name": "CC-BY-4.0", "url": "https://creativecommons.org/licenses/by/4.0/" }
  },
  "servers": [{ "url": "https://onagio.com/api", "description": "Production" }],
  "tags": [
    { "name": "cheese", "description": "The three wheels we make." },
    { "name": "museum", "description": "The typewriter collection and factory tours." },
    { "name": "meta", "description": "Service index and health." }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "getServiceIndex",
        "summary": "Service index",
        "description": "Returns the API name, version, documentation links, and the list of available endpoints. Call this first if you are discovering the API without the specification.",
        "tags": ["meta"],
        "responses": {
          "200": {
            "description": "Service index.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ServiceIndex" } } }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Health check",
        "description": "Reports whether the API is serving. Also reports how many aging rooms are online and how many typewriters in the collection currently work, because those are the two numbers anyone here actually cares about.",
        "tags": ["meta"],
        "responses": {
          "200": {
            "description": "Service is healthy.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } }
          }
        }
      }
    },
    "/cheeses": {
      "get": {
        "operationId": "listCheeses",
        "summary": "List all cheeses",
        "description": "Returns every wheel Onagio produces, with age, price per kilogram, tasting notes, and crystalline development. There are three. Use minAgeMonths to filter to the older wheels.",
        "tags": ["cheese"],
        "parameters": [
          {
            "name": "minAgeMonths",
            "in": "query",
            "required": false,
            "description": "Return only wheels aged at least this many months. Our range is 24 to 48.",
            "schema": { "type": "integer", "minimum": 0, "maximum": 48, "example": 36 }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of cheeses.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheeseList" } } }
          },
          "400": {
            "description": "minAgeMonths was not a non-negative number.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/cheeses/{sku}": {
      "get": {
        "operationId": "getCheeseBySku",
        "summary": "Get one cheese",
        "description": "Returns a single wheel by its SKU. SKUs are case-insensitive on input and always returned uppercase.",
        "tags": ["cheese"],
        "parameters": [{ "$ref": "#/components/parameters/Sku" }],
        "responses": {
          "200": {
            "description": "The requested cheese.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Cheese" } } }
          },
          "404": {
            "description": "No wheel with that SKU.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/cheeses/{sku}/availability": {
      "get": {
        "operationId": "getCheeseAvailability",
        "summary": "Get stock and lead time",
        "description": "Returns how many wheels of this SKU are uncommitted, the current lead time in days, and a coarse status. A status of allocated means the remaining wheels are already spoken for; the lead time then reflects when the next batch finishes aging, which for the 48-month wheel is measured in seasons.",
        "tags": ["cheese"],
        "parameters": [{ "$ref": "#/components/parameters/Sku" }],
        "responses": {
          "200": {
            "description": "Current availability.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Availability" } } }
          },
          "404": {
            "description": "No wheel with that SKU.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/cheeses/{sku}/pairings": {
      "get": {
        "operationId": "listCheesePairings",
        "summary": "List pairings for a cheese",
        "description": "Returns wine, food, and preparation suggestions for a wheel. These are opinions held by nine people in Parma, not nutritional guidance.",
        "tags": ["cheese"],
        "parameters": [{ "$ref": "#/components/parameters/Sku" }],
        "responses": {
          "200": {
            "description": "Pairing suggestions.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PairingList" } } }
          },
          "404": {
            "description": "No wheel with that SKU.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/typewriters": {
      "get": {
        "operationId": "listTypewriters",
        "summary": "List the typewriter collection",
        "description": "Returns the Olivetti machines kept in the building, their year, whether they work, and where they sit. Filter with working=true to get the machines visitors are allowed to type on.",
        "tags": ["museum"],
        "parameters": [
          {
            "name": "working",
            "in": "query",
            "required": false,
            "description": "Filter by whether the machine currently works.",
            "schema": { "type": "boolean", "example": true }
          }
        ],
        "responses": {
          "200": {
            "description": "The collection.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TypewriterList" } } }
          },
          "400": {
            "description": "working was not \"true\" or \"false\".",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/tours": {
      "get": {
        "operationId": "getTourSchedule",
        "summary": "Get the factory tour schedule",
        "description": "Returns the recurring weekly tour schedule, capacity, required notice, and the email address to book through. Tours are not booked through the API.",
        "tags": ["museum"],
        "responses": {
          "200": {
            "description": "The tour schedule.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TourSchedule" } } }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Sku": {
        "name": "sku",
        "in": "path",
        "required": true,
        "description": "The wheel's stock keeping unit.",
        "schema": {
          "type": "string",
          "enum": ["ONG-QWERTY-24", "ONG-LETTERA-36", "ONG-MANIFESTO-48"],
          "example": "ONG-LETTERA-36"
        }
      }
    },
    "schemas": {
      "Cheese": {
        "type": "object",
        "description": "One wheel of Parmigiano-Reggiano.",
        "required": ["sku", "name", "ageMonths", "milk", "designation", "wheelWeightKg", "priceEurPerKg", "tastingNotes", "crystalline", "description"],
        "properties": {
          "sku": { "type": "string", "description": "Stock keeping unit.", "example": "ONG-LETTERA-36" },
          "name": { "type": "string", "description": "Product name.", "example": "La Lettera d'Amore" },
          "ageMonths": { "type": "integer", "description": "Months aged.", "minimum": 24, "maximum": 48, "example": 36 },
          "milk": { "type": "string", "description": "Milk type.", "example": "raw cow" },
          "designation": { "type": "string", "description": "Protected designation.", "example": "Parmigiano-Reggiano DOP" },
          "wheelWeightKg": { "type": "number", "description": "Average finished wheel weight in kilograms.", "example": 36.9 },
          "priceEurPerKg": { "type": "number", "description": "Ex-works price in euro per kilogram, excluding VAT.", "example": 48 },
          "tastingNotes": { "type": "array", "description": "Dominant flavours.", "items": { "type": "string" }, "example": ["dried fruit", "broth", "toasted grain"] },
          "crystalline": { "type": "string", "description": "Extent of tyrosine crystal development.", "enum": ["light", "pronounced", "heavy"], "example": "pronounced" },
          "description": { "type": "string", "description": "Prose description of the wheel." }
        }
      },
      "CheeseList": {
        "type": "object",
        "required": ["count", "items"],
        "properties": {
          "count": { "type": "integer", "description": "Number of cheeses returned.", "example": 3 },
          "items": { "type": "array", "description": "The cheeses.", "items": { "$ref": "#/components/schemas/Cheese" } }
        }
      },
      "Availability": {
        "type": "object",
        "required": ["sku", "wheelsAvailable", "leadTimeDays", "status"],
        "properties": {
          "sku": { "type": "string", "example": "ONG-LETTERA-36" },
          "wheelsAvailable": { "type": "integer", "description": "Wheels not already committed to an order.", "minimum": 0, "example": 12 },
          "leadTimeDays": { "type": "integer", "description": "Days until dispatch. For allocated wheels this is when the next batch finishes aging.", "example": 7 },
          "status": { "type": "string", "description": "Coarse availability state.", "enum": ["in_stock", "low", "allocated"], "example": "in_stock" }
        }
      },
      "Pairing": {
        "type": "object",
        "required": ["with", "kind", "note"],
        "properties": {
          "with": { "type": "string", "description": "What to pair it with.", "example": "Barolo" },
          "kind": { "type": "string", "description": "Category of pairing.", "enum": ["wine", "food", "use"], "example": "wine" },
          "note": { "type": "string", "description": "Why." }
        }
      },
      "PairingList": {
        "type": "object",
        "required": ["sku", "count", "items"],
        "properties": {
          "sku": { "type": "string", "example": "ONG-LETTERA-36" },
          "count": { "type": "integer", "example": 3 },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/Pairing" } }
        }
      },
      "Typewriter": {
        "type": "object",
        "required": ["id", "model", "year", "working", "location", "note"],
        "properties": {
          "id": { "type": "string", "description": "Stable identifier.", "example": "oli-valentine-1969" },
          "model": { "type": "string", "description": "Manufacturer and model.", "example": "Olivetti Valentine" },
          "year": { "type": "integer", "description": "Year of manufacture.", "example": 1969 },
          "working": { "type": "boolean", "description": "Whether the machine currently types." },
          "location": { "type": "string", "description": "Where in the building it sits.", "enum": ["museum", "mezzanine", "office"], "example": "museum" },
          "note": { "type": "string", "description": "Anything worth knowing about this machine." }
        }
      },
      "TypewriterList": {
        "type": "object",
        "required": ["count", "items"],
        "properties": {
          "count": { "type": "integer", "example": 6 },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/Typewriter" } }
        }
      },
      "TourSlot": {
        "type": "object",
        "required": ["day", "times", "capacity"],
        "properties": {
          "day": { "type": "string", "description": "Day of the week.", "enum": ["tuesday", "thursday", "saturday"], "example": "tuesday" },
          "times": { "type": "array", "description": "Start times, 24-hour, Europe/Rome.", "items": { "type": "string" }, "example": ["11:00", "15:00"] },
          "capacity": { "type": "integer", "description": "Maximum visitors per slot.", "example": 10 }
        }
      },
      "TourSchedule": {
        "type": "object",
        "required": ["timezone", "bookingEmail", "noticeDays", "durationMinutes", "schedule", "note"],
        "properties": {
          "timezone": { "type": "string", "example": "Europe/Rome" },
          "bookingEmail": { "type": "string", "format": "email", "example": "o@onagio.com" },
          "noticeDays": { "type": "integer", "description": "Minimum days of notice required to book.", "example": 3 },
          "durationMinutes": { "type": "integer", "example": 60 },
          "schedule": { "type": "array", "items": { "$ref": "#/components/schemas/TourSlot" } },
          "note": { "type": "string", "description": "Closures and practical warnings." }
        }
      },
      "ServiceIndex": {
        "type": "object",
        "required": ["name", "version", "description", "documentation", "openapi", "endpoints"],
        "properties": {
          "name": { "type": "string", "example": "Onagio API" },
          "version": { "type": "string", "example": "1.2.0" },
          "description": { "type": "string" },
          "documentation": { "type": "string", "format": "uri", "example": "https://onagio.com/docs" },
          "openapi": { "type": "string", "format": "uri", "example": "https://onagio.com/openapi.json" },
          "endpoints": { "type": "array", "items": { "type": "string" } }
        }
      },
      "Health": {
        "type": "object",
        "required": ["status", "agingRoomsOnline", "typewritersWorking"],
        "properties": {
          "status": { "type": "string", "enum": ["ok"], "example": "ok" },
          "agingRoomsOnline": { "type": "integer", "example": 3 },
          "typewritersWorking": { "type": "integer", "example": 4 }
        }
      },
      "Error": {
        "type": "object",
        "description": "Every error from this API has this shape.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "resolution", "documentation"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code. Match on this, not on the message.",
                "enum": ["invalid_parameter", "cheese_not_found", "sub_resource_not_found", "endpoint_not_found", "method_not_allowed"],
                "example": "cheese_not_found"
              },
              "message": { "type": "string", "description": "What went wrong, in English." },
              "resolution": { "type": "string", "description": "What to do about it, including a concrete next call where one exists." },
              "documentation": { "type": "string", "format": "uri", "example": "https://onagio.com/docs" }
            }
          }
        }
      }
    }
  }
}
