{
  "openapi": "3.1.0",
  "info": {
    "title": "HSM-Siivouspalvelu quote-request API",
    "version": "1.0.0",
    "summary": "Request a home-cleaning (ylläpitosiivous) quote from HSM-Toimialapalvelu Oy",
    "description": "Submit a quote request (tarjouspyyntö) for recurring or one-time home cleaning in Vaasa, Laihia, Ilmajoki or Seinäjoki, Finland. Every accepted POST creates a REAL sales lead handled by a human — no test submissions. Rules for AI agents: submit only with the customer's informed consent and only the customer's real contact details; never fabricate data; one submission per customer intent; on network errors or 5xx retry with the SAME external_id (a {\"ok\":true,\"duplicate\":true} response means the earlier attempt already succeeded); identify yourself via the `agent` field. Validation error messages are in Finnish. The server computes the price estimate authoritatively from rooms + frequency + vat_free — pricing model: https://hsm-siivouspalvelu.fi/llms.txt. Scope: home cleaning only; for office cleaning, window washing or move-out cleaning have the customer call +358 50 413 1081 or email asiakaspalvelu@hsm-toimialapalvelu.fi. Personal data is processed per https://hsm-siivouspalvelu.fi/tietosuoja.html (DiiliPutki CRM, data stored in the EU).",
    "contact": {
      "name": "HSM-Toimialapalvelu Oy",
      "email": "asiakaspalvelu@hsm-toimialapalvelu.fi",
      "url": "https://hsm-siivouspalvelu.fi/yhteystiedot.html"
    }
  },
  "servers": [{ "url": "https://hsm-siivouspalvelu.fi" }],
  "paths": {
    "/api/lead": {
      "get": {
        "operationId": "getLeadApiUsage",
        "summary": "Machine-readable usage and discovery document for this API",
        "responses": {
          "200": {
            "description": "Usage summary: required/optional fields, rules for agents, response codes and documentation links.",
            "content": { "application/json": { "schema": { "type": "object" } } }
          }
        }
      },
      "post": {
        "operationId": "createLead",
        "summary": "Submit a cleaning quote request on a customer's behalf",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LeadRequest" },
              "examples": {
                "typical": {
                  "summary": "5-room home, cleaning every two weeks, submitted by an AI assistant",
                  "value": {
                    "name": "Matti Meikäläinen",
                    "email": "matti.meikalainen@example.com",
                    "phone": "+358 40 123 4567",
                    "address": "Esimerkkikatu 1 A 2",
                    "postal_code": "65100",
                    "city": "Vaasa",
                    "rooms": 5,
                    "frequency": "biweekly",
                    "vat_free": false,
                    "message": "Kaksi kissaa. Avain sovitaan erikseen.",
                    "agent": "Claude (Anthropic)",
                    "external_id": "hsm-hintalaskuri-3f2b7c1e-9d4a-4c0b-8f6e-1a2b3c4d5e6f"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Quote request accepted (or a retry of an already-accepted request).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "duplicate": {
                      "type": "boolean",
                      "description": "Present and true when the same external_id was already accepted earlier — treat as success, do NOT resubmit."
                    }
                  },
                  "required": ["ok"]
                },
                "examples": {
                  "accepted": { "value": { "ok": true } },
                  "duplicateRetry": { "value": { "ok": true, "duplicate": true } }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (error messages in Finnish) or malformed JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": false },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string", "enum": ["VALIDATION", "BAD_JSON"] },
                        "message": { "type": "string" }
                      }
                    },
                    "fields": {
                      "type": "object",
                      "description": "Per-field error messages (Finnish), e.g. {\"email\":\"Tarkista sähköpostiosoite\"}",
                      "additionalProperties": { "type": "string" }
                    }
                  }
                }
              }
            }
          },
          "405": { "description": "Method not allowed (only GET, POST, OPTIONS)." },
          "413": { "description": "Request body over 10 KB." },
          "500": { "description": "Unexpected server error." },
          "502": {
            "description": "Temporary upstream failure — retry later with the SAME external_id."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "LeadRequest": {
        "type": "object",
        "required": ["name", "email"],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "The customer's real name."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "The customer's real email address."
          },
          "form_type": {
            "type": "string",
            "enum": ["contact"],
            "description": "Set to 'contact' for a plain contact/tarjouspyyntö WITHOUT the calculator estimate: frequency is then not required and no draft quote is created (a human replies to the message). Omit for a normal quote request."
          },
          "frequency": {
            "type": "string",
            "enum": ["weekly", "biweekly", "monthly", "once"],
            "description": "Cleaning frequency: weekly, every two weeks, monthly, or one-time cleaning. REQUIRED unless form_type is 'contact'."
          },
          "rooms": {
            "type": "integer",
            "minimum": 1,
            "maximum": 15,
            "default": 3,
            "description": "Number of rooms (kitchen, living room, bedrooms — bathrooms/WC are not counted). Rounded and clamped server-side."
          },
          "vat_free": {
            "type": "boolean",
            "default": false,
            "description": "MUST be a JSON boolean (the string \"true\" is treated as false). Set true ONLY if the customer qualifies for the VAT-0% social service: functional capacity reduced due to old age, illness or disability."
          },
          "phone": { "type": "string", "maxLength": 50 },
          "address": { "type": "string", "maxLength": 200, "description": "Street address." },
          "postal_code": { "type": "string", "maxLength": 10 },
          "city": { "type": "string", "maxLength": 100 },
          "message": {
            "type": "string",
            "maxLength": 2000,
            "description": "Free text: pets, key handover, special wishes."
          },
          "agent": {
            "type": "string",
            "maxLength": 200,
            "description": "AI-assistant self-identification, e.g. 'Claude (Anthropic)'. Set this whenever an AI assistant submits on the customer's behalf — the submission is flagged for the seller."
          },
          "external_id": {
            "type": "string",
            "pattern": "^hsm-hintalaskuri-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
            "description": "Idempotency key: generate ONCE per customer intent and REUSE on retries. Values not matching the pattern are silently regenerated server-side (losing retry idempotency)."
          },
          "page_url": {
            "type": "string",
            "maxLength": 300,
            "pattern": "^https://",
            "description": "Page the request originated from; non-https values are dropped."
          },
          "utm_source": { "type": "string", "maxLength": 200 },
          "utm_medium": { "type": "string", "maxLength": 200 },
          "utm_campaign": { "type": "string", "maxLength": 200 },
          "utm_content": { "type": "string", "maxLength": 200 },
          "utm_term": { "type": "string", "maxLength": 200 }
        }
      }
    }
  }
}
