{
  "openapi": "3.1.0",
  "info": {
    "title": "Spanndecken ROTH – Richtpreis-API (Quote API)",
    "version": "1.2.0",
    "summary": "Non-binding stretch-ceiling price estimate, same formula as the website calculator.",
    "description": "Returns a non-binding price estimate (EUR) for a stretch ceiling (\"Spanndecke\") using exactly the formula of the public online calculator on spanndecken-roth.de. Responses never contain internal rates or factors — only the total, the echoed input, the provider identity and a disclaimer.\n\nTwo variants share one calculation:\n* `GET /public/quote` — no key, read-only, 30 requests/minute per IP.\n* `POST /quote` — static API key (`X-Api-Key` or `Authorization: Bearer`), 60 requests/minute per IP.\n\nAll results are sent with `Cache-Control: no-store`: always request a fresh quote. HTTPS only. An MCP server with the tool `calculate_spanndecken_price` offers the same calculation to MCP clients (see the README).\n\nOnly `area_m2` is required. Omitted parameters are filled in server-side with standard values — `category` A, `corners` 4, `spotlights` 0, `lamps` 0, `volume_discount` false — and the response lists them in `defaults_applied`; explicit values (including 0 / false) always win.",
    "contact": {
      "name": "Spanndecken ROTH",
      "url": "https://spanndecken-roth.de"
    }
  },
  "servers": [
    {
      "url": "https://spanndecken-roth.de/api/v1",
      "description": "Production"
    }
  ],
  "paths": {
    "/public/quote": {
      "get": {
        "operationId": "getPublicCeilingQuote",
        "summary": "Non-binding price estimate (public, no key)",
        "description": "Give the ceiling area in m² and the surface category (A, B or C). Corners, recessed spotlights, light fixtures and the volume discount are optional. The result is an estimate in whole euros.",
        "security": [],
        "parameters": [
          {
            "name": "area_m2",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 10000
            },
            "description": "Ceiling area in square metres (Deckenfläche). A decimal comma is accepted (35,5).",
            "example": 20
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "A",
                "B",
                "C"
              ],
              "default": "A"
            },
            "description": "Surface category: A = Lack / Matt / Satin (Farbtonauswahl), B = Metallic / Exclusive / Galaxy, C = Fotodruck. Optional; standard value A when omitted or empty.",
            "example": "A"
          },
          {
            "name": "corners",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "default": 4
            },
            "description": "Room corners (Ecken); 4 are included."
          },
          {
            "name": "spotlights",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 500,
              "default": 0
            },
            "description": "Recessed spotlights (Einbaustrahler)."
          },
          {
            "name": "lamps",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 500,
              "default": 0
            },
            "description": "Light fixtures / ceiling outlets (Leuchten)."
          },
          {
            "name": "volume_discount",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Apply the volume discount (Mengenrabatt). Accepts true/false/1/0."
          }
        ],
        "responses": {
          "200": {
            "description": "Estimate calculated",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                },
                "example": {
                  "$ref": "#/components/examples/QuoteExample/value"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (30/minute per IP)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Interface temporarily disabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/quote": {
      "post": {
        "operationId": "getCeilingQuote",
        "summary": "Non-binding price estimate (API key)",
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              },
              "examples": {
                "basic": {
                  "value": {
                    "area_m2": 20,
                    "category": "A"
                  }
                },
                "full": {
                  "value": {
                    "area_m2": 35.5,
                    "category": "C",
                    "corners": 6,
                    "spotlights": 8,
                    "lamps": 2,
                    "volume_discount": true
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Estimate calculated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not HTTPS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body larger than 16 KB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (60/minute per IP)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "API key not configured on the server",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key"
      },
      "BearerKey": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "QuoteRequest": {
        "type": "object",
        "required": [
          "area_m2"
        ],
        "properties": {
          "area_m2": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000,
            "description": "Ceiling area in m²; a decimal comma is accepted when sent as a string.",
            "example": 20
          },
          "category": {
            "type": "string",
            "enum": [
              "A",
              "B",
              "C"
            ],
            "description": "Surface category: A = Lack / Matt / Satin (Farbtonauswahl), B = Metallic / Exclusive / Galaxy, C = Fotodruck. Optional; standard value A when omitted or empty.",
            "default": "A"
          },
          "corners": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "default": 4
          },
          "spotlights": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500,
            "default": 0
          },
          "lamps": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500,
            "default": 0
          },
          "volume_discount": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "required": [
          "calculated_at",
          "defaults_applied",
          "input",
          "notice",
          "pricing_version",
          "provider",
          "quote",
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "provider": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "const": "Spanndecken ROTH"
              },
              "website": {
                "type": "string",
                "format": "uri"
              },
              "calculator_url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "quote": {
            "type": "object",
            "required": [
              "total",
              "currency",
              "formatted",
              "is_estimate"
            ],
            "properties": {
              "total": {
                "type": "integer",
                "description": "Estimate in whole euros"
              },
              "currency": {
                "type": "string",
                "const": "EUR"
              },
              "formatted": {
                "type": "string",
                "example": "926 €"
              },
              "is_estimate": {
                "type": "boolean",
                "const": true
              }
            }
          },
          "input": {
            "type": "object",
            "description": "Parameters the estimate was calculated with (defaults filled in).",
            "properties": {
              "area_m2": {
                "type": "number"
              },
              "category": {
                "type": "string",
                "enum": [
                  "A",
                  "B",
                  "C"
                ]
              },
              "category_label": {
                "type": "string"
              },
              "corners": {
                "type": "integer"
              },
              "spotlights": {
                "type": "integer"
              },
              "lamps": {
                "type": "integer"
              },
              "volume_discount": {
                "type": "boolean"
              }
            }
          },
          "notice": {
            "type": "object",
            "description": "Disclaimer to show the user (German and English).",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            }
          },
          "calculated_at": {
            "type": "string",
            "format": "date-time"
          },
          "pricing_version": {
            "type": "string",
            "description": "Date of the price basis used.",
            "example": "2026-09-26"
          },
          "defaults_applied": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "category",
                "corners",
                "spotlights",
                "lamps",
                "volume_discount"
              ]
            },
            "description": "Names of the parameters the server filled in with standard values because the request did not provide them; empty when every value was given explicitly."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "rate_limited"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ValidationError": {
        "type": "object",
        "required": [
          "error",
          "message",
          "errors"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "validation_failed"
          },
          "message": {
            "type": "string"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "examples": {
      "QuoteExample": {
        "value": {
          "status": "ok",
          "provider": {
            "name": "Spanndecken ROTH",
            "website": "https://spanndecken-roth.de",
            "calculator_url": "https://spanndecken-roth.de/de/preise"
          },
          "quote": {
            "total": 926,
            "currency": "EUR",
            "formatted": "926 €",
            "is_estimate": true
          },
          "input": {
            "area_m2": 20,
            "category": "A",
            "category_label": "Lack / Matt / Satin (Farbtonauswahl)",
            "corners": 4,
            "spotlights": 0,
            "lamps": 0,
            "volume_discount": false
          },
          "notice": {
            "de": "Unverbindlicher Richtpreis auf Basis des Online-Rechners von Spanndecken ROTH. Der verbindliche Preis wird nach Aufmaß vor Ort im Angebot genannt.",
            "en": "Non-binding estimate based on the Spanndecken ROTH online calculator. The binding price is given in the written offer after on-site measurement."
          },
          "calculated_at": "2026-10-23T12:00:00+02:00",
          "pricing_version": "2026-09-26",
          "defaults_applied": []
        }
      }
    }
  }
}
