{
  "openapi": "3.1.0",
  "info": {
    "title": "Oasys Corporations — superficie pública",
    "version": "1.0.0",
    "summary": "Endpoints públicos y archivos legibles por máquina del sitio de Oasys Corporations.",
    "description": "Este sitio es principalmente un sitio de contenido, no una API de datos. La especificación documenta lo que un agente puede consumir: los formularios públicos (contacto y newsletter), los archivos legibles por máquina (llms.txt, sitemap.xml, robots.txt, esta misma especificación) y la negociación de contenido `Accept: text/markdown` disponible en todas las páginas HTML.\n\nNo hay autenticación: todo lo publicado acá es público.\n\nFormato de error: las páginas y los errores del framework responden RFC 7807 (`application/problem+json`) cuando el cliente pide JSON. Los dos formularios responden su propio envoltorio (`{status, message, code, hint}`) por compatibilidad con el JavaScript del sitio.",
    "contact": {
      "name": "Oasys Corporations",
      "email": "info@oasyscorporations.com",
      "url": "https://oasyscorporations.com/#contact"
    },
    "license": {
      "name": "Contenido propietario — todos los derechos reservados",
      "url": "https://oasyscorporations.com/terminos_condiciones"
    }
  },
  "servers": [
    {
      "url": "https://oasyscorporations.com",
      "description": "Producción"
    }
  ],
  "externalDocs": {
    "description": "Índice del sitio para agentes",
    "url": "https://oasyscorporations.com/llms.txt"
  },
  "tags": [
    { "name": "contenido", "description": "Páginas HTML, disponibles también en markdown vía Accept." },
    { "name": "formularios", "description": "Envío de contacto y suscripción al newsletter." },
    { "name": "newsletter", "description": "Verificación y baja de suscripciones por correo." },
    { "name": "maquina", "description": "Archivos legibles por máquina." }
  ],
  "paths": {
    "/": {
      "get": {
        "tags": ["contenido"],
        "operationId": "getHome",
        "summary": "Página principal",
        "description": "Versión en inglés en `/en`. Con `Accept: text/markdown` devuelve la misma página en markdown.",
        "parameters": [{ "$ref": "#/components/parameters/Accept" }],
        "responses": {
          "200": { "$ref": "#/components/responses/PaginaHTML" },
          "406": { "$ref": "#/components/responses/NotAcceptable" }
        }
      }
    },
    "/servicios": {
      "get": {
        "tags": ["contenido"],
        "operationId": "getServicios",
        "summary": "Landing de servicios",
        "parameters": [{ "$ref": "#/components/parameters/Accept" }],
        "responses": {
          "200": { "$ref": "#/components/responses/PaginaHTML" },
          "406": { "$ref": "#/components/responses/NotAcceptable" }
        }
      }
    },
    "/servicios/{categoria}": {
      "get": {
        "tags": ["contenido"],
        "operationId": "getCategoriaServicio",
        "summary": "Página de una categoría de servicios",
        "parameters": [
          { "$ref": "#/components/parameters/Categoria" },
          { "$ref": "#/components/parameters/Accept" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/PaginaHTML" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "406": { "$ref": "#/components/responses/NotAcceptable" }
        }
      }
    },
    "/servicios/{categoria}/{servicio}": {
      "get": {
        "tags": ["contenido"],
        "operationId": "getServicio",
        "summary": "Página de un servicio individual",
        "description": "El listado completo de slugs válidos está en https://oasyscorporations.com/llms.txt y en el sitemap.",
        "parameters": [
          { "$ref": "#/components/parameters/Categoria" },
          {
            "name": "servicio",
            "in": "path",
            "required": true,
            "description": "Slug del servicio dentro de la categoría, por ejemplo `aplicaciones_web`.",
            "schema": { "type": "string", "pattern": "^[a-z0-9_]+$" }
          },
          { "$ref": "#/components/parameters/Accept" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/PaginaHTML" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "406": { "$ref": "#/components/responses/NotAcceptable" }
        }
      }
    },
    "/en": {
      "get": {
        "tags": ["contenido"],
        "operationId": "getHomeEn",
        "summary": "Página principal en inglés",
        "description": "Todas las páginas de marketing tienen su versión bajo el prefijo `/en`. Excepciones de slug: `/en/about-us`, `/en/privacy-policy`, `/en/terms-conditions`.",
        "parameters": [{ "$ref": "#/components/parameters/Accept" }],
        "responses": {
          "200": { "$ref": "#/components/responses/PaginaHTML" },
          "406": { "$ref": "#/components/responses/NotAcceptable" }
        }
      }
    },
    "/contacto": {
      "get": {
        "tags": ["formularios"],
        "operationId": "getContacto",
        "summary": "Redirección al formulario de contacto",
        "description": "El formulario vive en la página principal; este GET redirige a `/#contact`.",
        "responses": {
          "301": {
            "description": "Redirección permanente a https://oasyscorporations.com/#contact",
            "headers": {
              "Location": { "schema": { "type": "string", "format": "uri-reference" } }
            }
          }
        }
      },
      "post": {
        "tags": ["formularios"],
        "operationId": "enviarContacto",
        "summary": "Enviar una solicitud de contacto",
        "description": "El mensaje se envía por correo al equipo. Restricciones vigentes: la IP debe geolocalizar en GT, ES, MX o US y no puede venir de Tor; VPN, proxy y datacenter ya no bloquean, se marcan en el asunto del correo. El mensaje debe estar en español o en inglés (detección automática, solo por encima de 60 caracteres). La respuesta sale en el idioma que indique el campo `lang`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": { "$ref": "#/components/schemas/ContactoRequest" }
            },
            "multipart/form-data": {
              "schema": { "$ref": "#/components/schemas/ContactoRequest" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/FormularioOk" },
          "403": { "$ref": "#/components/responses/FormularioError" },
          "500": { "$ref": "#/components/responses/FormularioError" }
        }
      }
    },
    "/suscribirse": {
      "post": {
        "tags": ["formularios"],
        "operationId": "suscribirse",
        "summary": "Suscribirse al newsletter",
        "description": "Registra el correo y envía un mail de verificación. Rate limit: 5 por hora y 120 por día por IP. Aplican las mismas restricciones de origen que el formulario de contacto.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": { "$ref": "#/components/schemas/SuscripcionRequest" }
            },
            "multipart/form-data": {
              "schema": { "$ref": "#/components/schemas/SuscripcionRequest" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/FormularioOk" },
          "400": { "$ref": "#/components/responses/FormularioError" },
          "403": { "$ref": "#/components/responses/FormularioError" },
          "405": { "$ref": "#/components/responses/FormularioError" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/FormularioError" }
        }
      }
    },
    "/verificar/correo": {
      "get": {
        "tags": ["newsletter"],
        "operationId": "verificarCorreo",
        "summary": "Verificar una suscripción",
        "description": "Enlace enviado por correo al suscribirse.",
        "parameters": [{ "$ref": "#/components/parameters/IdSuscripcion" }],
        "responses": {
          "200": { "$ref": "#/components/responses/PaginaHTML" },
          "302": { "description": "Sin parámetro `id`: redirige a la página de error." },
          "500": { "$ref": "#/components/responses/FormularioError" }
        }
      }
    },
    "/dar_baja/correo": {
      "get": {
        "tags": ["newsletter"],
        "operationId": "darBajaCorreo",
        "summary": "Dar de baja una suscripción",
        "description": "Elimina el registro del correo. Enlace enviado en cada mail del newsletter.",
        "parameters": [{ "$ref": "#/components/parameters/IdSuscripcion" }],
        "responses": {
          "200": { "$ref": "#/components/responses/PaginaHTML" },
          "302": { "description": "Sin parámetro `id`: redirige a la página de error." },
          "500": { "$ref": "#/components/responses/FormularioError" }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": ["maquina"],
        "operationId": "getLlmsTxt",
        "summary": "Índice del sitio para agentes",
        "responses": {
          "200": {
            "description": "Índice en markdown, según la convención llms.txt.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "tags": ["maquina"],
        "operationId": "getSitemap",
        "summary": "Sitemap XML",
        "responses": {
          "200": {
            "description": "Sitemap en el esquema sitemaps.org 0.9.",
            "content": { "application/xml": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "tags": ["maquina"],
        "operationId": "getRobotsTxt",
        "summary": "Directivas para crawlers",
        "responses": {
          "200": {
            "description": "robots.txt.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": ["maquina"],
        "operationId": "getOpenapi",
        "summary": "Esta especificación",
        "description": "Disponible también en `/api/openapi.json`.",
        "responses": {
          "200": {
            "description": "Documento OpenAPI 3.1.",
            "content": { "application/json": { "schema": { "type": "object" } } }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Accept": {
        "name": "Accept",
        "in": "header",
        "required": false,
        "description": "`text/markdown` devuelve la página en markdown (`Vary: Accept`). Cualquier otro valor aceptable devuelve HTML. Un Accept que no admita ninguno de los tipos servidos devuelve 406.",
        "schema": {
          "type": "string",
          "examples": ["text/markdown", "text/html"]
        }
      },
      "Categoria": {
        "name": "categoria",
        "in": "path",
        "required": true,
        "description": "Categoría de servicio.",
        "schema": {
          "type": "string",
          "enum": [
            "desarrollo_software",
            "datos_ia",
            "redes",
            "servidores",
            "ciberseguridad",
            "marketing_digital"
          ]
        }
      },
      "IdSuscripcion": {
        "name": "id",
        "in": "query",
        "required": true,
        "description": "Identificador de la suscripción, enviado en el enlace del correo.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "PaginaHTML": {
        "description": "Página del sitio. En markdown si se negoció `Accept: text/markdown`.",
        "headers": {
          "Vary": {
            "description": "Siempre incluye `Accept`, para que ningún caché intermedio mezcle la variante HTML con la markdown.",
            "schema": { "type": "string", "examples": ["Accept, Accept-Encoding"] }
          }
        },
        "content": {
          "text/html": { "schema": { "type": "string" } },
          "text/markdown": { "schema": { "type": "string" } }
        }
      },
      "NotFound": {
        "description": "La ruta no existe.",
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" }
          },
          "text/markdown": { "schema": { "type": "string" } },
          "text/html": { "schema": { "type": "string" } }
        }
      },
      "NotAcceptable": {
        "description": "Ningún tipo de contenido pedido en Accept puede servirse.",
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" }
          }
        }
      },
      "RateLimited": {
        "description": "Se superó el límite de solicitudes.",
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" }
          }
        }
      },
      "FormularioOk": {
        "description": "El formulario se procesó.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/FormularioOk" }
          }
        }
      },
      "FormularioError": {
        "description": "El formulario fue rechazado.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/FormularioError" }
          }
        }
      }
    },
    "schemas": {
      "ContactoRequest": {
        "type": "object",
        "required": ["name", "email", "subject"],
        "properties": {
          "name": { "type": "string", "minLength": 1, "description": "Nombre de quien escribe." },
          "email": { "type": "string", "format": "email", "description": "Correo de respuesta." },
          "subject": { "type": "string", "minLength": 1, "description": "Servicio de interés. Sale de una lista cerrada; no se le aplica detección de idioma." },
          "message": { "type": "string", "description": "Mensaje. Opcional. Si supera los 60 caracteres debe estar en español o en inglés." },
          "phone": { "type": "string", "description": "Teléfono. Opcional. Formato local guatemalteco de 8 dígitos o E.164 con código de país." },
          "lang": { "type": "string", "enum": ["es", "en"], "default": "es", "description": "Idioma en el que se quiere la respuesta. Sin este campo se deduce del Referer y, si tampoco, se responde en español." }
        }
      },
      "SuscripcionRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string", "format": "email", "description": "Correo a suscribir." },
          "lang": { "type": "string", "enum": ["es", "en"], "default": "es", "description": "Idioma en el que se quiere la respuesta." }
        }
      },
      "FormularioOk": {
        "type": "object",
        "required": ["status", "message"],
        "properties": {
          "status": { "type": "string", "const": "OK" },
          "message": { "type": "string" }
        }
      },
      "FormularioError": {
        "type": "object",
        "required": ["status", "message", "code"],
        "description": "Envoltorio propio de los formularios, mantenido por compatibilidad con el JavaScript del sitio.",
        "properties": {
          "status": { "type": "string", "const": "ERROR" },
          "message": { "type": "string", "description": "Mensaje para mostrarle a una persona." },
          "code": {
            "type": "string",
            "description": "Código estable para discriminar el error desde código.",
            "enum": [
              "origen_no_verificable",
              "origen_no_permitido",
              "origen_bloqueado",
              "idioma_no_soportado",
              "campos_incompletos",
              "correo_invalido",
              "correo_ya_suscrito",
              "metodo_no_permitido",
              "error_interno"
            ]
          },
          "hint": { "type": "string", "description": "Qué hacer para que la próxima petición funcione." }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 7807 Problem Details, con `code`, `hint` y `documentation` agregados para agentes.",
        "required": ["type", "title", "status", "code", "detail"],
        "properties": {
          "type": { "type": "string", "format": "uri" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "code": { "type": "string", "description": "Código estable del error." },
          "detail": { "type": "string" },
          "hint": { "type": "string", "description": "Cómo recuperarse del error." },
          "instance": { "type": "string", "description": "Ruta que produjo el error." },
          "documentation": { "type": "string", "format": "uri" },
          "message": { "type": "string", "description": "Alias de `detail`, por compatibilidad." }
        }
      }
    }
  }
}
