{
  "openapi": "3.1.0",
  "info": {
    "title": "MenuQR Public & Integration REST API",
    "version": "1.0.0",
    "description": "Especificación oficial de la arquitectura de integración, API REST pública, autenticación OAuth 2.0 y servidor MCP de MenuQR (https://menuqr.rest). MenuQR es la plataforma SaaS líder de menús digitales interactivos con código QR para restaurantes en México y Latinoamérica.",
    "contact": {
      "name": "MenuQR Soporte Técnico & Desarrolladores",
      "url": "https://menuqr.rest/contacto",
      "email": "menuqr.rest@gmail.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://menuqr.rest/terminos-de-servicio"
    },
    "x-api-version": "1.0.0",
    "x-deprecation-policy": {
      "policy": "MenuQR uses URL path versioning (/api/v1). Breaking changes are introduced under new major version paths (/api/v2). Deprecated endpoints emit standard IETF Deprecation and Sunset headers with a minimum 6-month support window.",
      "sunsetPolicy": "Minimum 180 days notice from initial deprecation date.",
      "documentation": "https://menuqr.rest/docs"
    }
  },
  "servers": [
    {
      "url": "https://menuqr.rest",
      "description": "Servidor de producción (Vercel Edge / Global CDN)"
    },
    {
      "url": "https://menuqr.rest/api/v1/sandbox",
      "description": "Entorno sandbox para agentes de IA y pruebas de integración"
    }
  ],
  "security": [
    { "OAuth2": ["menu:read", "plans:read"] },
    { "BearerAuth": [] },
    { "ApiKeyAuth": [] },
    {}
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "summary": "Directorio raíz de la API REST pública",
        "description": "Descubrimiento de endpoints disponibles, sandbox, enlaces de documentación y estado operacional del servicio.",
        "operationId": "getApiV1Root",
        "security": [
          { "OAuth2": ["menu:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "responses": {
          "200": {
            "description": "Metadatos y lista de endpoints.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiRootResponse" }
              }
            }
          },
          "404": {
            "description": "Ruta no encontrada.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "summary": "Chequeo de salud del servicio",
        "description": "Estado de conectividad y disponibilidad de la API pública de MenuQR.",
        "operationId": "getHealth",
        "security": [
          { "OAuth2": ["menu:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "responses": {
          "200": {
            "description": "Estado del servicio operativo.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/v1/plans": {
      "get": {
        "summary": "Planes y precios vigentes en pesos mexicanos (MXN)",
        "description": "Devuelve la lista completa de planes (Gratis, Menú, Pedidos, Multisucursal), precios mensuales y anuales, y características incluidas.",
        "operationId": "getPublicPlans",
        "security": [
          { "OAuth2": ["plans:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "responses": {
          "200": {
            "description": "Catálogo de planes y precios en MXN.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PlansResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Error al consultar precios.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/v1/menus/{slug}": {
      "get": {
        "summary": "Menú digital de restaurante en JSON",
        "description": "Obtiene la carta estructurada de un restaurante en formato JSON para integraciones programáticas y agentes de IA.",
        "operationId": "getPublicMenuJson",
        "security": [
          { "OAuth2": ["menu:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Slug público del restaurante (ej. 'la-cantina-del-norte')",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Estructura del restaurante, categorías y platillos activos.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RestaurantMenuResponse" }
              }
            }
          },
          "400": {
            "description": "Parámetro slug faltante o inválido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Restaurante no encontrado o menú inactivo.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/v1/sandbox/menu": {
      "get": {
        "summary": "Menú de prueba en entorno Sandbox",
        "description": "Devuelve un menú completo de restaurante mexicano de prueba sin requerir autenticación ni slug real, optimizado para pruebas automáticas de agentes de IA.",
        "operationId": "getSandboxMenu",
        "security": [
          { "OAuth2": ["menu:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "responses": {
          "200": {
            "description": "Carta de restaurante de prueba en formato JSON completo.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SandboxMenuResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/token": {
      "post": {
        "summary": "Generación autoservicio de tokens de prueba para agentes",
        "description": "Genera tokens de acceso Bearer temporales sin fricción para que agentes y scripts autónomos puedan autenticarse de inmediato en el entorno sandbox.",
        "operationId": "generateSandboxToken",
        "security": [],
        "responses": {
          "200": {
            "description": "Token Bearer generado exitosamente para pruebas.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TokenResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "get": {
        "summary": "Consulta rápida de token de prueba vía GET",
        "description": "Atajo rápido para agentes que verifican conectividad con una solicitud GET.",
        "operationId": "getSandboxToken",
        "security": [],
        "responses": {
          "200": {
            "description": "Token Bearer generado exitosamente para pruebas.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TokenResponse" }
              }
            }
          }
        }
      }
    },
    "/api/hora": {
      "get": {
        "summary": "Hora del servidor sincronizada",
        "description": "Proporciona la marca de tiempo oficial del servidor para prevenir spoofing de reloj en pedidos y turnos.",
        "operationId": "getServerTime",
        "security": [
          { "OAuth2": ["menu:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "responses": {
          "200": {
            "description": "Marca de tiempo UTC e ISO.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ServerTimeResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "get": {
        "summary": "Metadatos del servidor Model Context Protocol (MCP)",
        "description": "Información sobre el transporte Streamable HTTP, versión del protocolo y herramientas soportadas.",
        "operationId": "getMcpMetadata",
        "security": [
          { "OAuth2": ["menu:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "responses": {
          "200": {
            "description": "Metadatos del servidor MCP.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/McpMetadataResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Endpoint JSON-RPC 2.0 para llamadas MCP",
        "description": "Permite inicializar la sesión MCP (`initialize`), listar herramientas (`tools/list`), y ejecutar herramientas (`tools/call`) para consultar cartas y planes.",
        "operationId": "postMcpRpc",
        "security": [
          { "OAuth2": ["menu:read"] },
          { "BearerAuth": [] },
          { "ApiKeyAuth": [] },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/McpRpcRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta JSON-RPC 2.0 con el resultado o error del protocolo MCP.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/McpRpcResponse" }
              }
            }
          },
          "400": {
            "description": "Error al parsear el JSON de la solicitud.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Límite de tasa excedido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/webhooks/polar": {
      "post": {
        "summary": "Recepción de eventos de suscripción de Polar",
        "description": "Webhook receptor de eventos de suscripciones y facturación firmado con Standard Webhooks.",
        "operationId": "polarWebhook",
        "security": [],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Evento procesado correctamente.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WebhookResponse" }
              }
            }
          },
          "400": {
            "description": "Firma inválida o payload corrupto.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Error interno del servidor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "OAuth2": {
        "type": "oauth2",
        "description": "OAuth 2.0 authorization framework supporting Authorization Code with PKCE and Client Credentials flows. Metadata published at /.well-known/oauth-authorization-server and RFC 9728 at /.well-known/oauth-protected-resource.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://menuqr.rest/auth/v1/authorize",
            "tokenUrl": "https://menuqr.rest/auth/v1/token",
            "scopes": {
              "menu:read": "Read public and private restaurant menu data, categories, and dishes",
              "menu:write": "Create, update, or remove dishes, categories, and prices",
              "orders:read": "View live and past restaurant orders and delivery statuses",
              "orders:write": "Create, accept, reject, or advance customer orders",
              "plans:read": "Query subscription tiers, pricing, and feature limits"
            }
          },
          "clientCredentials": {
            "tokenUrl": "https://menuqr.rest/auth/v1/token",
            "scopes": {
              "menu:read": "Read public and private restaurant menu data, categories, and dishes",
              "orders:read": "View live and past restaurant orders and delivery statuses",
              "plans:read": "Query subscription tiers, pricing, and feature limits"
            }
          }
        }
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Standard Bearer token (JWT) or sandbox token generated via /api/v1/auth/token."
      },
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Optional API Key for high-volume automated access and sandbox integration."
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "description": "Modelo estándar de respuesta de error estructurada para clientes y agentes",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Identificador único y legible por máquina del error",
                "example": "NOT_FOUND"
              },
              "message": {
                "type": "string",
                "description": "Descripción clara del motivo del error",
                "example": "The requested resource was not found."
              },
              "resolution": {
                "type": "string",
                "description": "Instrucciones accionables para resolver el error",
                "example": "Verify the endpoint in https://menuqr.rest/api/v1 or check https://menuqr.rest/docs."
              },
              "slug": {
                "type": "string",
                "description": "Parámetro slug asociado al error si aplica",
                "example": "la-cantina"
              }
            }
          }
        }
      },
      "ApiRootResponse": {
        "type": "object",
        "required": ["name", "version", "description", "endpoints", "status"],
        "properties": {
          "name": { "type": "string", "example": "MenuQR Public REST API" },
          "version": { "type": "string", "example": "1.0.0" },
          "description": { "type": "string" },
          "documentation": { "type": "string", "format": "uri" },
          "openapi": { "type": "string", "format": "uri" },
          "endpoints": {
            "type": "object",
            "properties": {
              "root": { "type": "string" },
              "health": { "type": "string" },
              "plans": { "type": "string" },
              "restaurant_menu": { "type": "string" },
              "sandbox_menu": { "type": "string" },
              "sandbox_token": { "type": "string" },
              "server_time": { "type": "string" },
              "mcp": { "type": "string" },
              "oauth_authorization_server": { "type": "string" },
              "oauth_protected_resource": { "type": "string" }
            }
          },
          "status": { "type": "string", "example": "operational" }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": ["status", "service", "version", "timestamp"],
        "properties": {
          "status": { "type": "string", "example": "ok" },
          "service": { "type": "string", "example": "MenuQR Public API" },
          "version": { "type": "string", "example": "1.0.0" },
          "timestamp": { "type": "string", "format": "date-time" },
          "documentation": { "type": "string", "format": "uri" },
          "openapi": { "type": "string", "format": "uri" }
        }
      },
      "PlanItem": {
        "type": "object",
        "required": ["id", "name", "currency", "price_monthly", "price_yearly", "features"],
        "properties": {
          "id": { "type": "string", "example": "orders" },
          "name": { "type": "string", "example": "Pedidos" },
          "tagline": { "type": "string" },
          "currency": { "type": "string", "example": "MXN" },
          "price_monthly": { "type": "number", "example": 749 },
          "price_yearly": { "type": "number", "example": 7490 },
          "features": {
            "type": "array",
            "items": { "type": "string" }
          }
        }
      },
      "PlansResponse": {
        "type": "object",
        "required": ["currency", "plans", "updated_at"],
        "properties": {
          "currency": { "type": "string", "example": "MXN" },
          "plans": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PlanItem" }
          },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "RestaurantMetadata": {
        "type": "object",
        "required": ["name", "slug", "currency"],
        "properties": {
          "name": { "type": "string", "example": "La Cantina" },
          "slug": { "type": "string", "example": "la-cantina" },
          "description": { "type": "string" },
          "address": { "type": "string" },
          "phone": { "type": "string" },
          "currency": { "type": "string", "example": "MXN" },
          "canonical_url": { "type": "string", "format": "uri" }
        }
      },
      "MenuMetadata": {
        "type": "object",
        "required": ["id", "name"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "order_table_enabled": { "type": "boolean" },
          "order_delivery_enabled": { "type": "boolean" }
        }
      },
      "MenuCategory": {
        "type": "object",
        "required": ["id", "name"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "icon": { "type": "string" },
          "empty_state_text": { "type": "string" }
        }
      },
      "MenuProduct": {
        "type": "object",
        "required": ["id", "name", "price"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "price": { "type": "number", "example": 120 },
          "formatted_price": { "type": "string", "example": "$120" },
          "category_id": { "type": "string" },
          "category": { "type": "string" },
          "image_url": { "type": "string", "format": "uri" },
          "allergens": { "type": "array", "items": { "type": "string" } },
          "tags": { "type": "array", "items": { "type": "string" } }
        }
      },
      "RestaurantMenuResponse": {
        "type": "object",
        "required": ["restaurant", "menu", "categories", "products"],
        "properties": {
          "restaurant": { "$ref": "#/components/schemas/RestaurantMetadata" },
          "menu": { "$ref": "#/components/schemas/MenuMetadata" },
          "categories": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuCategory" }
          },
          "products": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuProduct" }
          }
        }
      },
      "SandboxMenuResponse": {
        "type": "object",
        "required": ["restaurant", "menu", "categories", "products", "environment"],
        "properties": {
          "restaurant": { "$ref": "#/components/schemas/RestaurantMetadata" },
          "menu": { "$ref": "#/components/schemas/MenuMetadata" },
          "categories": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuCategory" }
          },
          "products": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuProduct" }
          },
          "environment": { "type": "string", "example": "sandbox" },
          "message": { "type": "string" }
        }
      },
      "TokenResponse": {
        "type": "object",
        "required": ["access_token", "token_type", "expires_in", "scope", "mode"],
        "properties": {
          "access_token": { "type": "string", "example": "menuqr_sandbox_live_token" },
          "token_type": { "type": "string", "example": "Bearer" },
          "expires_in": { "type": "integer", "example": 86400 },
          "scope": { "type": "string", "example": "menu:read orders:read plans:read" },
          "mode": { "type": "string", "example": "sandbox" },
          "documentation": { "type": "string", "format": "uri" },
          "message": { "type": "string" }
        }
      },
      "ServerTimeResponse": {
        "type": "object",
        "required": ["iso", "timestamp"],
        "properties": {
          "iso": { "type": "string", "format": "date-time" },
          "timestamp": { "type": "integer" }
        }
      },
      "McpMetadataResponse": {
        "type": "object",
        "required": ["name", "protocolVersion", "transport", "tools"],
        "properties": {
          "name": { "type": "string", "example": "MenuQR MCP Server" },
          "protocolVersion": { "type": "string", "example": "2024-11-05" },
          "transport": { "type": "string", "example": "Streamable HTTP" },
          "documentation": { "type": "string", "format": "uri" },
          "manifest": { "type": "string", "format": "uri" },
          "tools": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["name", "description"],
              "properties": {
                "name": { "type": "string" },
                "description": { "type": "string" }
              }
            }
          }
        }
      },
      "McpRpcRequest": {
        "type": "object",
        "required": ["jsonrpc", "method"],
        "properties": {
          "jsonrpc": { "type": "string", "example": "2.0" },
          "id": {
            "oneOf": [{ "type": "string" }, { "type": "number" }, { "type": "null" }]
          },
          "method": {
            "type": "string",
            "example": "tools/call",
            "enum": ["initialize", "tools/list", "tools/call", "ping"]
          },
          "params": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "arguments": { "type": "object" }
            }
          }
        }
      },
      "McpRpcResponse": {
        "type": "object",
        "required": ["jsonrpc"],
        "properties": {
          "jsonrpc": { "type": "string", "example": "2.0" },
          "id": {
            "oneOf": [{ "type": "string" }, { "type": "number" }, { "type": "null" }]
          },
          "result": { "type": "object" },
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "integer" },
              "message": { "type": "string" }
            }
          }
        }
      },
      "WebhookRequest": {
        "type": "object",
        "required": ["type", "data"],
        "properties": {
          "type": { "type": "string", "example": "subscription.created" },
          "data": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "status": { "type": "string" },
              "customer_id": { "type": "string" }
            }
          }
        }
      },
      "WebhookResponse": {
        "type": "object",
        "required": ["received"],
        "properties": {
          "received": { "type": "boolean", "example": true }
        }
      }
    }
  }
}
