{
  "openapi": "3.1.0",
  "info": {
    "title": "Fix OS — API pública de lectura",
    "version": "1.0.0",
    "summary": "Recursos públicos de solo lectura de Fix OS para agentes y desarrolladores.",
    "description": "Fix OS es un software de gestión en la nube para talleres de reparación y comercios de tecnología en México. Este contrato cubre únicamente la lectura pública: la disponibilidad del servicio. Las páginas de contenido de https://fixos.mx aceptan además `Accept: text/markdown` (declarando `Vary: Accept`) y sirven un gemelo `.md` de cada URL. No hay servidor MCP público ni API pública de escritura; operar un taller requiere iniciar sesión en la aplicación. Guía completa en https://fixos.mx/developers y https://fixos.mx/docs/auth.\n\n**Versionado**: por cabecera de RESPUESTA, no por ruta — `API-Version: 1` viaja en cada respuesta de este contrato. Un cambio incompatible sube ese número; uno compatible no. La URL (`/api/estado`) no cambia entre versiones.\n\n**Baja de endpoints**: un endpoint retirado responde `410 Gone` con la cabecera `Deprecation: true` y, en el cuerpo, el campo `rutaVigente` que dice a dónde ir. No hay un plazo de gracia anunciado por adelantado (`Sunset`): la baja ya ocurrió cuando se publica.\n\n**Cupo público**: las respuestas incluyen `RateLimit-Limit`, `RateLimit-Remaining` y `RateLimit-Reset` (segundos hasta que la ventana actual se reinicia) para que un cliente automatizado pueda auto-regularse. Al exceder el cupo, `429` incluye además `Retry-After`.\n\n**Errores**: cualquier ruta `/api/*` no documentada aquí (o un método no soportado) responde en `application/problem+json` siguiendo RFC 9457 — ver el esquema `Problem` más abajo. Nunca una página HTML ni texto plano.",
    "contact": {
      "name": "Soporte de Fix OS (WhatsApp, te contesta una persona)",
      "url": "https://fixos.mx/contact",
      "email": "fxservice@fixos.mx"
    },
    "license": {
      "name": "Uso de lectura pública",
      "url": "https://fixos.mx/legal"
    }
  },
  "externalDocs": {
    "description": "Guía para desarrolladores y agentes",
    "url": "https://fixos.mx/developers"
  },
  "servers": [
    { "url": "https://fixos.mx", "description": "Producción" }
  ],
  "tags": [
    { "name": "estado", "description": "Disponibilidad pública del servicio." }
  ],
  "paths": {
    "/api/estado": {
      "get": {
        "operationId": "getEstado",
        "tags": ["estado"],
        "summary": "Resumen público de disponibilidad",
        "description": "Devuelve el índice de disponibilidad de los últimos 30 días medido por la sonda de Fix OS, la última verificación y hasta 5 avisos de incidente publicados por el equipo. No requiere autenticación. Es el mismo JSON que consume la página https://fixos.mx/estado. Respuesta cacheada ~60 s en el borde. Cupo público: 60 solicitudes/minuto por IP (ver cabeceras RateLimit-* en cada respuesta).",
        "security": [],
        "parameters": [
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "schema": { "type": "string", "default": "application/json" }
          },
          {
            "name": "API-Version",
            "in": "header",
            "required": false,
            "description": "Versión del contrato contra la que quieres integrar. Omítela para recibir siempre la vigente. Si pides una que no servimos, la respuesta es 400 `unsupported_api_version` en vez de la vigente disfrazada — el servidor la NEGOCIA, no sólo la anuncia. La misma cabecera viaja en cada respuesta.",
            "schema": { "type": "string", "enum": ["1"], "default": "1" }
          }
        ],
        "responses": {
          "200": {
            "description": "Resumen de disponibilidad. `midiendo` distingue el estado: `false` significa que la medición pública aún no arranca o no hay lectura (`sinLectura: true`).",
            "headers": {
              "API-Version": { "schema": { "type": "string" }, "description": "Versión del contrato de esta ruta. Hoy: 1." },
              "RateLimit-Limit": { "schema": { "type": "integer" }, "description": "Cupo máximo de la ventana actual (60/min)." },
              "RateLimit-Remaining": { "schema": { "type": "integer" }, "description": "Solicitudes que quedan en la ventana actual." },
              "RateLimit-Reset": { "schema": { "type": "integer" }, "description": "Segundos hasta que la ventana actual se reinicia." }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EstadoResumen" },
                "examples": {
                  "midiendo": {
                    "summary": "Sonda activa",
                    "value": {
                      "midiendo": true,
                      "pct": 99.87,
                      "corridas": 1440,
                      "excluidas": 0,
                      "inicio": "2026-06-01",
                      "ultima": { "at": "2026-09-10T18:07:00.000Z", "nivel": "ok" },
                      "dias": [],
                      "componentes": [],
                      "avisos": []
                    }
                  },
                  "calibrando": {
                    "summary": "Medición pública sin arrancar",
                    "value": { "midiendo": false, "avisos": [] }
                  },
                  "sinLectura": {
                    "summary": "El medidor no respondió (no confirma una caída del servicio)",
                    "value": { "midiendo": false, "sinLectura": true }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Método no permitido. Solo se admiten GET y OPTIONS.",
            "headers": {
              "Allow": { "schema": { "type": "string" }, "description": "GET, OPTIONS" },
              "API-Version": { "schema": { "type": "string" } },
              "RateLimit-Limit": { "schema": { "type": "integer" } },
              "RateLimit-Remaining": { "schema": { "type": "integer" } },
              "RateLimit-Reset": { "schema": { "type": "integer" } }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "error": { "type": "string" } }
                }
              }
            }
          },
          "400": {
            "description": "Pediste una versión del contrato que no se sirve (cabecera `API-Version`).",
            "headers": {
              "API-Version": { "schema": { "type": "string" }, "description": "La versión que SÍ se sirve." }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" },
                "example": {
                  "type": "https://fixos.mx/openapi.json#/components/schemas/Problem",
                  "title": "Versión de API no soportada",
                  "status": 400,
                  "detail": "Pediste la versión \"2\" y este contrato sirve 1. Omite la cabecera API-Version para recibir siempre la vigente.",
                  "code": "unsupported_api_version"
                }
              }
            }
          },
          "429": {
            "description": "Cupo público excedido (60/min por IP).",
            "headers": {
              "Retry-After": { "schema": { "type": "integer" }, "description": "Segundos a esperar antes de reintentar." },
              "RateLimit-Limit": { "schema": { "type": "integer" } },
              "RateLimit-Remaining": { "schema": { "type": "integer" }, "description": "0 en esta respuesta." },
              "RateLimit-Reset": { "schema": { "type": "integer" } }
            },
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" },
                "example": {
                  "type": "https://fixos.mx/openapi.json#/components/schemas/Problem",
                  "title": "Demasiadas solicitudes",
                  "status": 429,
                  "detail": "Superaste el cupo público de esta ruta. Reintenta tras el tiempo indicado en Retry-After.",
                  "code": "too_many_requests"
                }
              }
            }
          },
          "default": {
            "description": "Cualquier otro código (p. ej. 404 en una ruta /api/* no reconocida, o 5xx) responde este mismo esquema tipado.",
            "content": {
              "application/problem+json": {
                "schema": { "$ref": "#/components/schemas/Problem" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 (Problem Details for HTTP APIs). `code` es la clave ESTABLE para programar contra ella; `title`/`detail` son para un humano o un LLM.",
        "required": ["title", "status", "code"],
        "properties": {
          "type": { "type": "string", "format": "uri", "description": "URI que identifica este tipo de error." },
          "title": { "type": "string", "description": "Resumen corto, independiente del caso concreto." },
          "status": { "type": "integer", "description": "El mismo código HTTP de la respuesta." },
          "detail": { "type": "string", "description": "Detalle específico de esta respuesta." },
          "code": { "type": "string", "description": "Identificador estable en snake_case, p. ej. \"not_found\" o \"too_many_requests\"." }
        }
      },
      "EstadoResumen": {
        "type": "object",
        "additionalProperties": true,
        "required": ["midiendo"],
        "properties": {
          "midiendo": {
            "type": "boolean",
            "description": "true si la sonda ya produce una medición pública."
          },
          "sinLectura": {
            "type": "boolean",
            "description": "true si no se pudo leer el estado actual. No confirma una caída del servicio."
          },
          "pct": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Índice de disponibilidad de los últimos 30 días (porcentaje de verificaciones correctas)."
          },
          "corridas": {
            "type": "integer",
            "description": "Verificaciones consideradas en la ventana de 30 días."
          },
          "excluidas": {
            "type": "integer",
            "description": "Verificaciones inválidas del monitor excluidas del cálculo; su evidencia se conserva."
          },
          "inicio": {
            "type": "string",
            "description": "Fecha desde la que se mide (YYYY-MM-DD)."
          },
          "ultima": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "at": { "type": "string", "format": "date-time" },
              "nivel": { "type": "string", "description": "Nivel de la última verificación, p. ej. ok / degradado / caido." }
            }
          },
          "dias": {
            "type": "array",
            "description": "Serie diaria para la tira de barras de /estado.",
            "items": { "type": "object", "additionalProperties": true }
          },
          "componentes": {
            "type": "array",
            "description": "Estado por componente monitoreado.",
            "items": { "type": "object", "additionalProperties": true }
          },
          "avisos": {
            "type": "array",
            "description": "Hasta 5 avisos de incidente visibles, del más reciente al más antiguo.",
            "items": { "$ref": "#/components/schemas/Aviso" }
          }
        }
      },
      "Aviso": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "created_at": { "type": "string", "format": "date-time" },
          "estado": { "type": "string", "description": "Clasificación del aviso." },
          "titulo": { "type": "string" },
          "cuerpo": { "type": "string" }
        }
      }
    }
  }
}
