{
  "openapi": "3.1.0",
  "info": {
    "title": "Lunelet optional website registration",
    "version": "1.0.0",
    "description": "Explicit owner consent is required. Publish /.well-known/lunelet.json on the actual canonical HTTPS website before calling. No book or visitor data. No API key or public list. Read https://lunelet.com/register.md."
  },
  "servers": [{"url": "https://lunelet.com"}, {"url": "https://lunelet.pages.dev", "description": "Same-registry mirror"}],
  "paths": {
    "/api/registrations": {
      "post": {
        "operationId": "registerWebsite",
        "summary": "Register or reconfirm an explicitly opted-in website",
        "description": "Call from the website agent only, after the owner agrees and the shelf and consent file are live. The proof must have registered:true. Repeated calls update one record and preserve registeredAt. Do not call on visits or ordinary book updates.",
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OptIn"}}}},
        "responses": {
          "200": {"description": "Registration saved", "content": {"application/json": {"schema": {"type": "object", "required": ["registered", "site", "design", "registeredAt", "lastConfirmedAt", "consentVersion"], "properties": {"registered": {"const": true}, "site": {"type": "string", "format": "uri"}, "design": {"type": "string"}, "registeredAt": {"type": "string", "format": "date-time"}, "lastConfirmedAt": {"type": "string", "format": "date-time"}, "consentVersion": {"const": "2026-09-15"}}}}}},
          "400": {"description": "Invalid input or missing explicit consent"},
          "403": {"description": "Do not register from visitor-side JavaScript"},
          "413": {"description": "Request or proof too large"},
          "415": {"description": "Use application/json"},
          "422": {"description": "Public DNS or consent proof could not be verified"},
          "502": {"description": "DNS checker unavailable"},
          "503": {"description": "Temporarily unavailable; the shelf is unaffected"}
        }
      },
      "delete": {
        "operationId": "withdrawWebsite",
        "summary": "Withdraw the exact website's registration",
        "description": "Only after the owner requests withdrawal, publish registered:false in the consent file, then call this endpoint. On registered:false success the row is deleted and the owner may remove the consent file. Books are not changed.",
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OptOut"}}}},
        "responses": {
          "200": {"description": "Removal confirmed; idempotent", "content": {"application/json": {"schema": {"type": "object", "required": ["registered", "site"], "properties": {"registered": {"const": false}, "site": {"type": "string", "format": "uri"}}}}}},
          "400": {"description": "Invalid input or missing explicit withdrawal"},
          "403": {"description": "Cross-origin browser request refused"},
          "413": {"description": "Request or proof too large"},
          "415": {"description": "Use application/json"},
          "422": {"description": "Published opt-out could not be verified; existing registration remains"},
          "502": {"description": "DNS checker unavailable"},
          "503": {"description": "Temporarily unavailable; do not claim removal succeeded"}
        }
      }
    }
  },
  "components": {"schemas": {
    "OptIn": {"type": "object", "additionalProperties": false, "required": ["site", "consent"], "properties": {"site": {"type": "string", "format": "uri", "maxLength": 300, "description": "Canonical public HTTPS origin only, without a port, credentials, path, query or fragment"}, "consent": {"const": true}}},
    "OptOut": {"type": "object", "additionalProperties": false, "required": ["site", "consent"], "properties": {"site": {"type": "string", "format": "uri", "maxLength": 300}, "consent": {"const": false}}}
  }}
}
