{
  "openapi": "3.1.0",
  "info": {
    "title": "Doctorweb Content API",
    "version": "1.0.0",
    "summary": "Interfaz de solo lectura para leer el contenido de doctorweb.agency en Markdown o JSON.",
    "description": "Doctorweb es una **agencia de marketing medico**, no un producto de software.\n\nEsta especificacion **no** describe una API de negocio: no hay endpoints para contratar servicios, consultar leads ni operar una cuenta, y no existen claves de API que solicitar. Lo que describe es la superficie de **lectura de contenido** que el sitio implementa realmente, para que un agente pueda consumir cada pagina publica como Markdown o como JSON estructurado en lugar de raspar HTML.\n\nTodas las operaciones son `GET`, publicas, sin autenticacion y sin efectos secundarios.\n\n## Como funciona\n\nCada pagina publica existe en tres representaciones equivalentes:\n\n| Representacion | Como obtenerla |\n| --- | --- |\n| `text/html` | Peticion normal de navegador |\n| `text/markdown` | Cabecera `Accept: text/markdown`, o sufijo `.md` |\n| `application/json` | Cabecera `Accept: application/json`, o sufijo `.json` |\n\nLa negociacion sigue RFC 9110 seccion 12.5.1 y la convencion de https://acceptmarkdown.com/: se respetan los q-values y la especificidad de los rangos, toda respuesta negociable lleva `Vary: Accept`, y un `Accept` que no admita ningun tipo disponible recibe `406`.\n\n## Errores\n\nLos errores se devuelven como JSON estructurado (`code`, `message`, `hint`) cuando la peticion admite `application/json`; en caso contrario como Markdown legible. Nunca como una pagina HTML de error opaca.\n\n## Limites\n\nNo hay limite de peticiones publicado ni cuota por cliente. Se pide un uso razonable y un `User-Agent` identificable. Para volumenes altos o rastreos completos, usa `/sitemap.xml` en lugar de recorrer enlaces.\n\n## Contratar servicios\n\nNo es programatico. La via es escribir a mrivera@doctorweb.agency o agendar una reunion en https://www.doctorweb.agency/contact.",
    "termsOfService": "https://www.doctorweb.agency/terms",
    "contact": {
      "name": "Doctorweb",
      "email": "mrivera@doctorweb.agency",
      "url": "https://www.doctorweb.agency/developers"
    },
    "x-audience": "Agentes de IA, rastreadores y desarrolladores que necesiten leer el contenido de Doctorweb de forma estructurada."
  },
  "externalDocs": {
    "description": "Recursos para desarrolladores y agentes de Doctorweb",
    "url": "https://www.doctorweb.agency/developers"
  },
  "servers": [
    {
      "url": "https://www.doctorweb.agency",
      "description": "Produccion. El apex doctorweb.agency responde 301 hacia este host."
    }
  ],
  "tags": [
    {
      "name": "Contenido",
      "description": "Lectura de las paginas publicas en HTML, Markdown o JSON."
    },
    {
      "name": "Descubrimiento",
      "description": "Ficheros legibles por maquina que describen el sitio: indice para agentes, instrucciones, mapa del sitio y esta especificacion."
    }
  ],
  "paths": {
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "tags": ["Descubrimiento"],
        "summary": "Especificacion OpenAPI de esta interfaz",
        "description": "Devuelve este mismo documento. Publicado tambien en `/openapi`, `/api/openapi.json` y `/.well-known/openapi.json`.",
        "responses": {
          "200": {
            "description": "Documento OpenAPI 3.1.",
            "content": {
              "application/json": {
                "schema": { "type": "object" }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getAgentIndex",
        "tags": ["Descubrimiento"],
        "summary": "Indice curado del sitio para agentes",
        "description": "Indice en formato llmstxt.org: resumen de la entidad, seccion explicita de cuando usar Doctorweb, y listas de enlaces por tema. Es el mejor punto de entrada para un agente que llega al sitio por primera vez.",
        "responses": {
          "200": {
            "description": "Indice en Markdown plano.",
            "content": {
              "text/plain": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "/agent-instructions.md": {
      "get": {
        "operationId": "getAgentInstructions",
        "tags": ["Descubrimiento"],
        "summary": "Instrucciones de uso para agentes",
        "description": "Guia operativa: para que trabajos Doctorweb es la opcion correcta, en que casos NO derivar aqui, como debe actuar un agente y el contrato exacto de las respuestas.",
        "responses": {
          "200": {
            "description": "Instrucciones en Markdown.",
            "content": {
              "text/markdown": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "operationId": "getSitemap",
        "tags": ["Descubrimiento"],
        "summary": "Mapa del sitio",
        "description": "Lista completa de URLs canonicas indexables, con `lastmod` y `priority`. Usalo para enumerar paginas en lugar de recorrer enlaces.",
        "responses": {
          "200": {
            "description": "Sitemap XML conforme al esquema sitemaps.org 0.9.",
            "content": {
              "application/xml": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "operationId": "getRobotsPolicy",
        "tags": ["Descubrimiento"],
        "summary": "Politica de rastreo",
        "description": "Reglas de rastreo y punteros al sitemap y a los ficheros para agentes.",
        "responses": {
          "200": {
            "description": "Politica en texto plano.",
            "content": {
              "text/plain": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "/{page}.json": {
      "get": {
        "operationId": "getPageAsJson",
        "tags": ["Contenido"],
        "summary": "Pagina como objeto JSON estructurado",
        "description": "Devuelve la pagina como objeto tipado: identidad, metadatos, el cuerpo en Markdown y los enlaces a sus representaciones alternativas. Es la forma mas comoda de consumir el contenido desde codigo.\n\nEquivale a pedir la pagina con `Accept: application/json`.",
        "parameters": [{ "$ref": "#/components/parameters/PageSlug" }],
        "responses": {
          "200": {
            "description": "Pagina serializada.",
            "headers": {
              "Vary": { "$ref": "#/components/headers/Vary" },
              "Link": { "$ref": "#/components/headers/Link" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Page" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{page}.md": {
      "get": {
        "operationId": "getPageAsMarkdown",
        "tags": ["Contenido"],
        "summary": "Pagina como Markdown",
        "description": "Devuelve el cuerpo de la pagina en Markdown, sin navegacion, scripts ni estilos. Encabezado, resumen, URL canonica e idioma van al principio del documento.\n\nEquivale a pedir la pagina con `Accept: text/markdown`.",
        "parameters": [{ "$ref": "#/components/parameters/PageSlug" }],
        "responses": {
          "200": {
            "description": "Pagina en Markdown.",
            "headers": {
              "Vary": { "$ref": "#/components/headers/Vary" },
              "Link": { "$ref": "#/components/headers/Link" }
            },
            "content": {
              "text/markdown": {
                "schema": { "type": "string" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/{page}.html": {
      "get": {
        "operationId": "getPageNegotiated",
        "tags": ["Contenido"],
        "summary": "Pagina con negociacion de contenido",
        "description": "Devuelve la pagina en la representacion que mejor case con el encabezado `Accept`. Un navegador recibe HTML; un agente que pida `text/markdown` o `application/json` recibe esa representacion sobre la misma URL.\n\nLa seleccion sigue RFC 9110 seccion 12.5.1: gana el mayor q-value y, a igualdad, el rango mas especifico. `q=0` descarta el tipo. La respuesta siempre incluye `Vary: Accept`.",
        "parameters": [
          { "$ref": "#/components/parameters/PageSlug" },
          { "$ref": "#/components/parameters/AcceptHeader" }
        ],
        "responses": {
          "200": {
            "description": "Pagina en la representacion negociada.",
            "headers": {
              "Vary": { "$ref": "#/components/headers/Vary" },
              "Link": { "$ref": "#/components/headers/Link" }
            },
            "content": {
              "text/html": { "schema": { "type": "string" } },
              "text/markdown": { "schema": { "type": "string" } },
              "application/json": { "schema": { "$ref": "#/components/schemas/Page" } }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "406": { "$ref": "#/components/responses/NotAcceptable" }
        }
      }
    },
    "/{route}": {
      "get": {
        "operationId": "getCanonicalRoute",
        "tags": ["Contenido"],
        "summary": "Rutas limpias estables",
        "description": "Rutas cortas y estables que los agentes prueban por convencion. Sirven la pagina correspondiente sin redirigir (respuesta `200`, no `301`), y admiten la misma negociacion de contenido que el resto.\n\nCada ruta acepta ademas su alias en espanol: `/acerca-de` y `/nosotros` para `/about`, `/desarrolladores` y `/agents` para `/developers`, `/plans` para `/pricing`, `/privacy-policy` para `/privacy`, y `/docs` y `/api` para `/developers`.",
        "parameters": [
          { "$ref": "#/components/parameters/CanonicalRoute" },
          { "$ref": "#/components/parameters/AcceptHeader" }
        ],
        "responses": {
          "200": {
            "description": "Pagina en la representacion negociada.",
            "headers": {
              "Vary": { "$ref": "#/components/headers/Vary" },
              "Link": { "$ref": "#/components/headers/Link" }
            },
            "content": {
              "text/html": { "schema": { "type": "string" } },
              "text/markdown": { "schema": { "type": "string" } },
              "application/json": { "schema": { "$ref": "#/components/schemas/Page" } }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "406": { "$ref": "#/components/responses/NotAcceptable" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "PageSlug": {
        "name": "page",
        "in": "path",
        "required": true,
        "description": "Identificador de la pagina, sin extension. La lista completa esta en `/sitemap.xml` y las paginas destacadas en `/llms.txt`.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._-]+$",
          "examples": ["planes", "marketing-medico", "about", "developers", "faq"]
        }
      },
      "CanonicalRoute": {
        "name": "route",
        "in": "path",
        "required": true,
        "description": "Ruta limpia estable.",
        "schema": {
          "type": "string",
          "enum": ["about", "contact", "privacy", "terms", "pricing", "developers", "docs", "api"]
        }
      },
      "AcceptHeader": {
        "name": "Accept",
        "in": "header",
        "required": false,
        "description": "Tipo de medio deseado. Admite q-values, por ejemplo `text/markdown;q=1.0, text/html;q=0.8`. Si se omite, se devuelve HTML.",
        "schema": {
          "type": "string",
          "default": "text/html",
          "examples": [
            "text/markdown",
            "application/json",
            "text/markdown;q=1.0, text/html;q=0.8"
          ]
        }
      }
    },
    "headers": {
      "Vary": {
        "description": "Siempre incluye `Accept`, para que ninguna cache sirva la representacion equivocada.",
        "schema": { "type": "string", "examples": ["Accept, Accept-Encoding"] }
      },
      "Link": {
        "description": "Enlaces `rel=\"alternate\"` a las demas representaciones, `rel=\"help\"` a `/llms.txt` y `rel=\"service-desc\"` a `/openapi.json`.",
        "schema": { "type": "string" }
      }
    },
    "schemas": {
      "Page": {
        "type": "object",
        "title": "Page",
        "description": "Representacion estructurada de una pagina publica de doctorweb.agency.",
        "required": ["slug", "path", "url", "title", "language", "content_type", "content", "alternates"],
        "additionalProperties": false,
        "properties": {
          "slug": {
            "type": "string",
            "description": "Identificador de la pagina, sin extension.",
            "examples": ["planes"]
          },
          "path": {
            "type": "string",
            "description": "Ruta absoluta en el sitio.",
            "examples": ["/planes.html"]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL canonica absoluta.",
            "examples": ["https://www.doctorweb.agency/planes.html"]
          },
          "title": {
            "type": "string",
            "description": "Titulo de la pagina, el mismo del elemento <title>."
          },
          "description": {
            "type": "string",
            "description": "Resumen de la pagina, el mismo de <meta name=\"description\">."
          },
          "language": {
            "type": "string",
            "description": "Codigo de idioma BCP 47 del contenido.",
            "examples": ["es"]
          },
          "site": {
            "type": "string",
            "description": "Nombre de la entidad propietaria.",
            "const": "Doctorweb"
          },
          "content_type": {
            "type": "string",
            "description": "Formato del campo `content`.",
            "const": "text/markdown"
          },
          "content": {
            "type": "string",
            "description": "Cuerpo de la pagina en Markdown, sin navegacion ni pie."
          },
          "alternates": {
            "type": "object",
            "description": "URLs de las demas representaciones de esta misma pagina.",
            "required": ["text/html", "text/markdown", "application/json"],
            "additionalProperties": false,
            "properties": {
              "text/html": { "type": "string", "format": "uri" },
              "text/markdown": { "type": "string", "format": "uri" },
              "application/json": { "type": "string", "format": "uri" }
            }
          },
          "agent_resources": {
            "type": "object",
            "description": "Ficheros de descubrimiento del sitio, repetidos en cada pagina para que un agente que aterrice en cualquier URL sepa donde seguir.",
            "additionalProperties": false,
            "properties": {
              "openapi": { "type": "string", "format": "uri" },
              "llms_txt": { "type": "string", "format": "uri" },
              "agent_instructions": { "type": "string", "format": "uri" },
              "sitemap": { "type": "string", "format": "uri" }
            }
          },
          "generated_by": {
            "type": "string",
            "description": "Herramienta que produjo el documento."
          }
        }
      },
      "Error": {
        "type": "object",
        "title": "Error",
        "description": "Error estructurado. `code` es estable y apto para ramificar en codigo; `message` y `hint` son para leer.",
        "required": ["error"],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "object",
            "required": ["status", "code", "message"],
            "additionalProperties": false,
            "properties": {
              "status": {
                "type": "integer",
                "description": "Codigo de estado HTTP de la respuesta.",
                "examples": [404]
              },
              "code": {
                "type": "string",
                "description": "Identificador estable del error.",
                "enum": ["not_found", "not_acceptable"]
              },
              "message": {
                "type": "string",
                "description": "Que ha ocurrido, en una frase."
              },
              "hint": {
                "type": "string",
                "description": "Como resolverlo: que pedir o donde mirar a continuacion."
              },
              "documentation_url": {
                "type": "string",
                "format": "uri",
                "description": "Pagina de recursos para desarrolladores y agentes."
              },
              "requested_path": {
                "type": "string",
                "description": "Ruta que provoco el error."
              }
            }
          },
          "resources": {
            "type": "object",
            "description": "Puntos de entrada para recuperarse del error.",
            "additionalProperties": false,
            "properties": {
              "openapi": { "type": "string", "format": "uri" },
              "llms_txt": { "type": "string", "format": "uri" },
              "agent_instructions": { "type": "string", "format": "uri" },
              "sitemap": { "type": "string", "format": "uri" }
            }
          }
        }
      }
    },
    "responses": {
      "NotFound": {
        "description": "No existe ningun recurso publicado en esa ruta. El cuerpo enlaza de vuelta al sitemap y al indice para agentes.",
        "headers": {
          "Vary": { "$ref": "#/components/headers/Vary" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "status": 404,
                "code": "not_found",
                "message": "No existe ningun recurso publicado en esta ruta.",
                "hint": "Consulta /sitemap.xml para la lista completa de URLs, o /llms.txt para el indice curado del sitio.",
                "documentation_url": "https://www.doctorweb.agency/developers",
                "requested_path": "/ruta-que-no-existe"
              },
              "resources": {
                "openapi": "https://www.doctorweb.agency/openapi.json",
                "llms_txt": "https://www.doctorweb.agency/llms.txt",
                "agent_instructions": "https://www.doctorweb.agency/agent-instructions.md",
                "sitemap": "https://www.doctorweb.agency/sitemap.xml"
              }
            }
          },
          "text/markdown": { "schema": { "type": "string" } },
          "text/html": { "schema": { "type": "string" } }
        }
      },
      "NotAcceptable": {
        "description": "Ninguna representacion disponible casa con el encabezado `Accept`. El cuerpo lista los tipos que si estan disponibles.",
        "headers": {
          "Vary": { "$ref": "#/components/headers/Vary" }
        },
        "content": {
          "text/markdown": { "schema": { "type": "string" } }
        }
      }
    }
  }
}
