{
  "openapi": "3.1.0",
  "info": {
    "title": "agent-audit-api",
    "version": "1.0.0",
    "summary": "API pública de auditoría agent-readiness de lab.kntor.io",
    "description": "Audita qué tan preparado está un sitio para agentes de IA (nivel 0–5) más SEO on-page e inteligencia del sitio. Flujo asíncrono: POST /audit devuelve un uuid y el cliente hace polling a GET /audit/{uuid} hasta status \"done\". El informe accionable pagado se compra vía POST /checkout (Mercado Pago) y se entrega por email con un link /report/{token}. Sin autenticación; CORS abierto.",
    "contact": { "url": "https://lab.kntor.io", "email": "edgar.gomero@gmail.com" }
  },
  "servers": [{ "url": "https://lab.kntor.io" }],
  "paths": {
    "/audit": {
      "post": {
        "operationId": "startAudit",
        "summary": "Inicia una auditoría (o devuelve la cacheada)",
        "description": "Si existe una auditoría del dominio con menos de 7 días y no se pide refresh, responde 200 con el resultado cacheado (no consume cuota). Si no, inicia un scan nuevo y responde 202 con un uuid para polling. Límites: 50 scans nuevos por IP al día y 5000 scans globales al mes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "description": "URL o dominio a auditar (se normaliza a https)", "examples": ["https://ejemplo.com"] },
                  "refresh": { "type": "boolean", "default": false, "description": "true fuerza un scan nuevo aunque exista caché fresca" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado cacheado (auditoría con menos de 7 días)",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["status", "cached", "result"],
              "properties": {
                "status": { "type": "string", "const": "done" },
                "cached": { "type": "boolean", "const": true },
                "result": { "$ref": "#/components/schemas/AuditResult" }
              }
            } } }
          },
          "202": {
            "description": "Scan nuevo iniciado; hacer polling a pollUrl cada 10–15 s",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["status", "uuid", "pollUrl"],
              "properties": {
                "status": { "type": "string", "const": "scanning" },
                "uuid": { "type": "string", "format": "uuid" },
                "pollUrl": { "type": "string", "examples": ["/audit/0e3f6f0a-2f43-4c2e-9a3e-1c2d3e4f5a6b"] }
              }
            } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "414": { "description": "URL demasiado larga", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": {
            "description": "Límite alcanzado. retryable=true con header retry-after (3600 s si es límite por IP, 11 s si el scanner upstream está saturado); retryable=false si se agotó la cuota mensual.",
            "headers": { "retry-after": { "schema": { "type": "string" }, "description": "Segundos sugeridos antes de reintentar (ausente si no es reintentable)" } },
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["error"],
              "properties": {
                "error": { "type": "string" },
                "retryable": { "type": "boolean" },
                "month": { "type": "string", "description": "Mes de la cuota agotada (solo en cuota mensual)", "examples": ["2026-06"] }
              }
            } } }
          },
          "502": { "description": "Error del scanner upstream", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/audit/{uuid}": {
      "get": {
        "operationId": "pollAudit",
        "summary": "Consulta el estado/resultado de una auditoría",
        "parameters": [{
          "name": "uuid", "in": "path", "required": true,
          "schema": { "type": "string", "format": "uuid", "pattern": "^[0-9a-f-]{36}$" },
          "description": "uuid devuelto por POST /audit"
        }],
        "responses": {
          "200": {
            "description": "Estado del job: scanning (reintentar según retry-after), done (con result) o error",
            "headers": { "retry-after": { "schema": { "type": "string", "const": "15" }, "description": "Presente mientras status=scanning" } },
            "content": { "application/json": { "schema": { "oneOf": [
              {
                "title": "Scanning",
                "type": "object",
                "required": ["status", "uuid"],
                "properties": { "status": { "type": "string", "const": "scanning" }, "uuid": { "type": "string", "format": "uuid" } }
              },
              {
                "title": "Done",
                "type": "object",
                "required": ["status", "result"],
                "properties": { "status": { "type": "string", "const": "done" }, "result": { "$ref": "#/components/schemas/AuditResult" } }
              },
              {
                "title": "Error",
                "type": "object",
                "required": ["status", "error"],
                "properties": { "status": { "type": "string", "const": "error" }, "error": { "type": "string" } }
              }
            ] } } }
          },
          "404": { "description": "Job no encontrado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/history/{domain}": {
      "get": {
        "operationId": "getHistory",
        "summary": "Historial de auditorías de un dominio",
        "parameters": [{
          "name": "domain", "in": "path", "required": true,
          "schema": { "type": "string" },
          "description": "Dominio o URL (se normaliza)", "example": "ejemplo.com"
        }],
        "responses": {
          "200": {
            "description": "Hasta 50 auditorías, más recientes primero",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["domain", "audits"],
              "properties": {
                "domain": { "type": "string" },
                "audits": { "type": "array", "items": { "$ref": "#/components/schemas/AuditResult" } }
              }
            } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/quota": {
      "get": {
        "operationId": "getQuota",
        "summary": "Uso de la cuota mensual global de scans",
        "responses": {
          "200": {
            "description": "Estado de la cuota del mes en curso",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["month", "used", "budget", "remaining"],
              "properties": {
                "month": { "type": "string", "examples": ["2026-06"] },
                "used": { "type": "integer" },
                "budget": { "type": "integer", "examples": [5000] },
                "remaining": { "type": "integer", "minimum": 0 }
              }
            } } }
          }
        }
      }
    },
    "/lead": {
      "post": {
        "operationId": "createLead",
        "summary": "Registra un lead (email + consentimiento)",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["email", "consent"],
            "properties": {
              "email": { "type": "string", "format": "email", "maxLength": 254 },
              "url": { "type": "string", "description": "URL o dominio de interés (alternativa: domain)" },
              "domain": { "type": "string", "description": "Dominio de interés (alternativa: url). Se requiere url o domain." },
              "scanUuid": { "type": "string", "format": "uuid", "description": "uuid del scan asociado, si existe" },
              "consent": { "type": "boolean", "const": true, "description": "Debe ser exactamente true" },
              "source": { "type": "string", "description": "Origen del lead (libre)" }
            }
          } } }
        },
        "responses": {
          "201": {
            "description": "Lead creado",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["leadId"],
              "properties": { "leadId": { "type": "string", "format": "uuid" } }
            } } }
          },
          "400": { "description": "Email inválido, falta consentimiento o URL/dominio inválido", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/summary": {
      "post": {
        "operationId": "sendSummary",
        "summary": "Envía por email el resumen de una auditoría gratuita y registra el lead",
        "description": "Resuelve la auditoría por scanUuid (preferido) o por url (la más reciente del dominio), registra un lead y envía al correo el resumen del scoring: nivel, scores y qué checks fallan. El CÓMO arreglarlos es exclusivo del informe pagado. Idempotente por correo+scan: repetir responde 200 con already=true sin reenviar. Límite: 10 envíos por IP al día.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["email", "consent"],
            "properties": {
              "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Correo donde llega el resumen" },
              "scanUuid": { "type": "string", "format": "uuid", "description": "uuid del scan auditado (preferido)" },
              "url": { "type": "string", "description": "URL o dominio auditado; fallback si no hay scanUuid (usa la auditoría más reciente)" },
              "consent": { "type": "boolean", "const": true, "description": "Debe ser exactamente true" }
            }
          } } }
        },
        "responses": {
          "201": {
            "description": "Lead creado. sent=true si el correo salió; sent=false si el envío falló (el lead queda registrado igual)",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["leadId", "sent"],
              "properties": {
                "leadId": { "type": "string", "format": "uuid" },
                "sent": { "type": "boolean" }
              }
            } } }
          },
          "200": {
            "description": "Ya se había enviado el resumen de ese scan a ese correo; no se reenvía",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["sent", "already"],
              "properties": {
                "sent": { "type": "boolean", "const": false },
                "already": { "type": "boolean", "const": true }
              }
            } } }
          },
          "400": { "description": "Email inválido, falta consentimiento o URL inválida", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "No existe una auditoría para ese scanUuid/url", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": {
            "description": "Límite de 10 envíos por IP al día alcanzado",
            "headers": { "retry-after": { "schema": { "type": "string", "const": "3600" } } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/checkout": {
      "post": {
        "operationId": "createCheckout",
        "summary": "Crea una orden de compra del informe y devuelve el link de pago de Mercado Pago",
        "description": "Precio: $9.990 CLP. El agente debe entregar checkoutUrl al humano para que pague. Tras la confirmación del pago, el informe se envía por email con un link /report/{token}.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["email"],
            "properties": {
              "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Email donde llegará el informe" },
              "url": { "type": "string", "description": "URL o dominio auditado (alternativa: domain). Se requiere url o domain." },
              "domain": { "type": "string" },
              "scanUuid": { "type": "string", "format": "uuid", "description": "Scan exacto a usar en el informe; si se omite se usa la auditoría más reciente del dominio" },
              "leadId": { "type": "string", "format": "uuid", "description": "Lead previo asociado, si existe" },
              "coupon": { "type": "string", "description": "Código de descuento (opcional). Valídalo antes con POST /coupon/validate; si es inválido el checkout responde 400." }
            }
          } } }
        },
        "responses": {
          "201": {
            "description": "Orden creada con link de pago",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["orderId", "checkoutUrl", "finalAmount", "discount"],
              "properties": {
                "orderId": { "type": "string", "format": "uuid" },
                "checkoutUrl": { "type": "string", "format": "uri", "description": "Link de pago de Mercado Pago para el humano" },
                "sandboxUrl": { "type": "string", "format": "uri", "description": "Link de pago en sandbox (pruebas)" },
                "finalAmount": { "type": "integer", "description": "Monto a cobrar (CLP), con descuento aplicado si hubo cupón" },
                "discount": { "type": "integer", "description": "Descuento aplicado (0 sin cupón)" }
              }
            } } }
          },
          "400": { "description": "Email, URL/dominio o cupón inválido", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Precio no configurado / error interno", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/coupon/validate": {
      "post": {
        "operationId": "validateCoupon",
        "summary": "Cotiza un código de descuento contra el precio del informe",
        "description": "Sin efectos: solo informa si el código es válido y cuánto quedaría el precio. Útil para mostrar el precio rebajado antes de crear el checkout.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["code"],
            "properties": { "code": { "type": "string", "description": "Código de descuento" } }
          } } }
        },
        "responses": {
          "200": {
            "description": "Cotización (valid=false incluye reason legible)",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["valid", "code", "discount", "finalAmount"],
              "properties": {
                "valid": { "type": "boolean" },
                "code": { "type": "string", "description": "Código normalizado (mayúsculas)" },
                "discount": { "type": "integer" },
                "finalAmount": { "type": "integer" },
                "reason": { "type": "string", "description": "Motivo cuando valid=false" }
              }
            } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/report/{token}": {
      "get": {
        "operationId": "getReport",
        "summary": "Informe pagado",
        "description": "El token (48 hex) llega por email tras la confirmación del pago. Devuelve 404 si el token no existe o la orden no está pagada.",
        "parameters": [{
          "name": "token", "in": "path", "required": true,
          "schema": { "type": "string", "pattern": "^[0-9a-f]{48}$" }
        }],
        "responses": {
          "200": {
            "description": "Informe del dominio comprado",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["paid", "domain", "report"],
              "properties": {
                "paid": { "type": "boolean", "const": true },
                "domain": { "type": "string" },
                "report": { "$ref": "#/components/schemas/AuditResult" }
              }
            } } }
          },
          "404": { "description": "Token inexistente, orden no pagada o informe no disponible", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "responses": {
      "BadRequest": {
        "description": "Entrada inválida",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "retryable": { "type": "boolean" }
        }
      },
      "CheckStatus": { "type": "string", "enum": ["pass", "fail", "neutral"] },
      "CategoryScore": {
        "type": "object",
        "required": ["passed", "applicable", "score"],
        "properties": {
          "passed": { "type": "integer" },
          "applicable": { "type": "integer" },
          "score": { "type": "integer", "minimum": 0, "maximum": 100 }
        }
      },
      "AgentReadiness": {
        "type": "object",
        "required": ["recognized", "level", "levelName", "overall", "categories", "statuses"],
        "properties": {
          "recognized": { "type": "boolean", "description": "false si el reporte llegó pero no se reconoció el bloque agent-readiness (level/overall quedan null)" },
          "level": { "type": ["integer", "null"], "minimum": 0, "maximum": 5, "description": "0 Not Ready · 1 Basic Web Presence · 2 Bot-Aware · 3 Agent-Readable · 4 Agent-Integrated · 5 Agent-Native" },
          "levelName": { "type": "string" },
          "overall": { "type": ["integer", "null"], "minimum": 0, "maximum": 100, "description": "Score global 0–100" },
          "categories": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/CategoryScore" }, "description": "Score por categoría (discoverability, contentAccessibility, botAccessControl, discovery, commerce)" },
          "statuses": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/CheckStatus" }, "description": "Estado por check id (robotsTxt, sitemap, linkHeaders, dnsAid, markdownNegotiation, robotsTxtAiRules, contentSignals, webBotAuth, apiCatalog, oauthDiscovery, oauthProtectedResource, authMd, mcpServerCard, a2aAgentCard, agentSkills, webMcp, x402, mpp, ucp, acp, ap2)" },
          "raw": { "description": "Bloque crudo del scanner" }
        }
      },
      "SeoCheck": {
        "type": "object",
        "required": ["id", "status", "detail"],
        "properties": {
          "id": { "type": "string" },
          "status": { "$ref": "#/components/schemas/CheckStatus" },
          "detail": { "type": "string" }
        }
      },
      "SeoResult": {
        "type": "object",
        "required": ["score", "checks", "extracted"],
        "properties": {
          "score": { "type": "integer", "minimum": 0, "maximum": 100 },
          "checks": { "type": "array", "items": { "$ref": "#/components/schemas/SeoCheck" } },
          "extracted": { "type": "object", "additionalProperties": true, "description": "Datos extraídos de la página (title, meta description, headings, etc.)" }
        }
      },
      "SiteIntel": {
        "type": "object",
        "properties": {
          "popularity": {
            "type": ["object", "null"],
            "properties": {
              "rank": { "type": ["integer", "null"] },
              "bucket": { "type": ["string", "null"] }
            }
          },
          "category": { "type": ["string", "null"] },
          "technologies": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["name", "categories"],
              "properties": {
                "name": { "type": "string" },
                "categories": { "type": "array", "items": { "type": "string" } }
              }
            }
          },
          "hosting": {
            "type": ["object", "null"],
            "properties": {
              "ip": { "type": ["string", "null"] },
              "asn": { "type": ["string", "null"] },
              "asnName": { "type": ["string", "null"] },
              "country": { "type": ["string", "null"] },
              "city": { "type": ["string", "null"] },
              "server": { "type": ["string", "null"] }
            }
          },
          "domain": {
            "type": ["object", "null"],
            "properties": {
              "registrar": { "type": ["string", "null"] },
              "createdDate": { "type": ["string", "null"] },
              "expirationDate": { "type": ["string", "null"] },
              "nameservers": { "type": "array", "items": { "type": "string" } },
              "dnssec": { "type": ["boolean", "null"] }
            }
          }
        }
      },
      "AuditResult": {
        "type": "object",
        "required": ["domain", "url", "ts", "scanUuid", "visibility", "agentReadiness", "seo", "intel"],
        "properties": {
          "domain": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "ts": { "type": "string", "format": "date-time" },
          "scanUuid": { "type": ["string", "null"], "format": "uuid" },
          "visibility": { "type": "string", "examples": ["public"] },
          "agentReadiness": { "oneOf": [{ "$ref": "#/components/schemas/AgentReadiness" }, { "type": "null" }] },
          "seo": { "oneOf": [{ "$ref": "#/components/schemas/SeoResult" }, { "type": "null" }] },
          "intel": { "oneOf": [{ "$ref": "#/components/schemas/SiteIntel" }, { "type": "null" }] }
        }
      }
    }
  }
}
