{ "openapi": "3.1.0", "info": { "title": "Molinet cal Flari — API pública de la tienda", "version": "2026-08-26", "summary": "Lectura del catálogo, búsqueda y carrito de la almazara Molinet cal Flari.", "description": "Superficie pública y sin autenticación de la tienda Shopify de Molinet cal Flari (aceite de oliva virgen extra arbequina D.O.P. Les Garrigues, Cervià de les Garrigues, Lleida).\n\nTodos los endpoints de lectura funcionan sin credenciales: un agente puede consultar catálogo, precios y disponibilidad sin registrarse.\n\nPara **comprar**, esta tienda implementa el Universal Commerce Protocol (UCP) sobre MCP. El descubrimiento está en `/.well-known/ucp` y el servidor MCP en `POST /api/ucp/mcp`. Ahí es donde se crean y pagan los pedidos; esta especificación no duplica esas operaciones porque son JSON-RPC, no REST.\n\nLa API la sirve la plataforma Shopify, no un backend propio.\n\n## Versionado y obsolescencia\n\nEste documento se versiona **por ruta**: la version estable vive en `https://molinetcalflari.com/openapi-v1.json`, y `/openapi.json` apunta siempre a la ultima. Un cambio incompatible se publica en `/openapi-v2.json` y la ruta anterior sigue sirviendose al menos 6 meses. (La forma `/v1/...` no es posible aqui: Shopify solo resuelve redirecciones de un unico tramo de ruta.) El campo `info.version` lleva la fecha ISO de la revision.\n\nLa capa de comercio se versiona aparte, por el propio protocolo UCP: las versiones admitidas se enumeran en `GET /.well-known/ucp` bajo `supported_versions`, cada una con su endpoint. Un agente debe negociar la version ahi y no dar por hecha la mas reciente.\n\nLos endpoints de catalogo (`/products.json` y companeros) son superficie de plataforma de Shopify: su contrato lo fija Shopify, no esta tienda.", "contact": { "name": "Molinet cal Flari", "email": "info@molinetdecalflari.com", "url": "https://molinetcalflari.com/pages/contact" }, "license": { "name": "Uso público de lectura", "identifier": "LicenseRef-public-read" } }, "externalDocs": { "description": "Instrucciones para agentes (llms.txt) y perfil UCP", "url": "https://molinetcalflari.com/llms.txt" }, "servers": [ { "url": "https://molinetcalflari.com", "description": "Mercado España (es, ca)" }, { "url": "https://molinetcalflari.com/de-de", "description": "Mercado Alemania (de)" }, { "url": "https://molinetcalflari.com/fr-fr", "description": "Mercado Francia (fr)" }, { "url": "https://molinetcalflari.com/fr-be", "description": "Mercado Bélgica (fr)" }, { "url": "https://molinetcalflari.com/en-uk", "description": "Mercado Reino Unido (en)" }, { "url": "https://molinetcalflari.com/en-pb", "description": "Mercado Países Bajos (en)" }, { "url": "https://molinetcalflari.com/de-su", "description": "Mercado Suiza (de)" } ], "security": [ {} ], "tags": [ { "name": "catalogo", "description": "Productos y colecciones. Sin autenticación." }, { "name": "busqueda", "description": "Sugerencias de búsqueda. Sin autenticación." }, { "name": "carrito", "description": "Carrito de la sesión. Sin autenticación; usa cookie de sesión." }, { "name": "comercio-agentico", "description": "Compra vía UCP sobre MCP." } ], "paths": { "/products.json": { "get": { "operationId": "listarProductos", "security": [ {} ], "tags": [ "catalogo" ], "summary": "Lista los productos publicados", "description": "Devuelve los productos del escaparate con sus variantes, precios e imágenes. El precio va como cadena decimal en euros, por ejemplo \"59.90\". Paginado por `page` y `limit`.", "parameters": [ { "name": "limit", "in": "query", "required": false, "description": "Productos por página (1-250, por defecto 30).", "schema": { "type": "integer", "minimum": 1, "maximum": 250, "default": 30 } }, { "name": "page", "in": "query", "required": false, "description": "Página, empezando en 1.", "schema": { "type": "integer", "minimum": 1, "default": 1 } } ], "responses": { "200": { "description": "Lista de productos.", "content": { "application/json": { "schema": { "type": "object", "required": [ "products" ], "properties": { "products": { "type": "array", "items": { "$ref": "#/components/schemas/Producto" } } } } } } }, "404": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "default": { "$ref": "#/components/responses/Error" } } } }, "/collections.json": { "get": { "operationId": "listarColecciones", "security": [ {} ], "tags": [ "catalogo" ], "summary": "Lista las colecciones publicadas", "description": "Devuelve las colecciones del escaparate. La colección principal del catálogo es `catalogo`; también existen `aceites-arbequina`, `vinagres-y-miel` y `camisetas-pod`.", "parameters": [ { "name": "limit", "in": "query", "required": false, "description": "Colecciones por página (1-250).", "schema": { "type": "integer", "minimum": 1, "maximum": 250, "default": 30 } }, { "name": "page", "in": "query", "required": false, "description": "Página, empezando en 1.", "schema": { "type": "integer", "minimum": 1, "default": 1 } } ], "responses": { "200": { "description": "Lista de colecciones.", "content": { "application/json": { "schema": { "type": "object", "required": [ "collections" ], "properties": { "collections": { "type": "array", "items": { "$ref": "#/components/schemas/Coleccion" } } } } } } }, "404": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "default": { "$ref": "#/components/responses/Error" } } } }, "/collections/{handle}/products.json": { "get": { "operationId": "listarProductosDeColeccion", "security": [ {} ], "tags": [ "catalogo" ], "summary": "Lista los productos de una colección", "description": "Devuelve los productos de la colección indicada. Atención: si el handle no existe, Shopify responde 200 con una lista vacía, no 404.", "parameters": [ { "name": "handle", "in": "path", "required": true, "description": "Identificador de la colección, por ejemplo `catalogo`.", "schema": { "type": "string", "pattern": "^[a-z0-9-]+$" } }, { "name": "limit", "in": "query", "required": false, "description": "Productos por página (1-250).", "schema": { "type": "integer", "minimum": 1, "maximum": 250, "default": 30 } }, { "name": "page", "in": "query", "required": false, "description": "Página, empezando en 1.", "schema": { "type": "integer", "minimum": 1, "default": 1 } } ], "responses": { "200": { "description": "Lista de productos de la colección; vacía si el handle no existe.", "content": { "application/json": { "schema": { "type": "object", "required": [ "products" ], "properties": { "products": { "type": "array", "items": { "$ref": "#/components/schemas/Producto" } } } } } } }, "429": { "$ref": "#/components/responses/Error" }, "default": { "$ref": "#/components/responses/Error" } } } }, "/search/suggest.json": { "get": { "operationId": "sugerirBusqueda", "security": [ {} ], "tags": [ "busqueda" ], "summary": "Sugerencias de búsqueda", "description": "Busca productos, colecciones, páginas y artículos por texto. Útil para resolver el nombre de un producto antes de añadirlo al carrito.", "parameters": [ { "name": "q", "in": "query", "required": true, "description": "Texto a buscar, por ejemplo `aceite arbequina`.", "schema": { "type": "string", "minLength": 1 } }, { "name": "resources[type]", "in": "query", "required": false, "description": "Tipos separados por coma: product, collection, page, article.", "schema": { "type": "string", "default": "product" } }, { "name": "resources[limit]", "in": "query", "required": false, "description": "Resultados por tipo (1-10).", "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 10 } } ], "responses": { "200": { "description": "Resultados agrupados por tipo de recurso.", "content": { "application/json": { "schema": { "type": "object", "required": [ "resources" ], "properties": { "resources": { "type": "object", "properties": { "results": { "type": "object", "additionalProperties": true } } } } } } } }, "422": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "default": { "$ref": "#/components/responses/Error" } } } }, "/cart.js": { "get": { "operationId": "obtenerCarrito", "security": [ {} ], "tags": [ "carrito" ], "summary": "Devuelve el carrito de la sesión", "description": "Devuelve el carrito asociado a la cookie de sesión. Los importes van en céntimos como enteros: 5990 son 59,90 €. Responde con JSON pero con Content-Type `text/javascript`, que es como lo sirve Shopify.", "responses": { "200": { "description": "Carrito actual.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Carrito" } } } }, "429": { "$ref": "#/components/responses/Error" }, "default": { "$ref": "#/components/responses/Error" } } } }, "/recommendations/products.json": { "get": { "operationId": "recomendarProductos", "security": [ {} ], "tags": [ "catalogo" ], "summary": "Productos recomendados a partir de uno dado", "description": "Devuelve productos relacionados con el indicado. Si el producto no está publicado, responde 404 con el sobre de error estándar.", "parameters": [ { "name": "product_id", "in": "query", "required": true, "description": "Identificador numérico del producto.", "schema": { "type": "integer", "format": "int64" } }, { "name": "limit", "in": "query", "required": false, "description": "Número de recomendaciones (1-10).", "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 4 } }, { "name": "intent", "in": "query", "required": false, "description": "Criterio de recomendación.", "schema": { "type": "string", "enum": [ "related", "complementary" ], "default": "related" } } ], "responses": { "200": { "description": "Productos recomendados.", "content": { "application/json": { "schema": { "type": "object", "required": [ "products" ], "properties": { "products": { "type": "array", "items": { "$ref": "#/components/schemas/Producto" } } } } } } }, "404": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "default": { "$ref": "#/components/responses/Error" } } } }, "/api/ucp/mcp": { "post": { "operationId": "llamarMcpUcp", "security": [ {}, { "cuentaDeCliente": [ "openid", "email", "customer-account-api:full", "customer-account-mcp-api:full" ] } ], "tags": [ "comercio-agentico" ], "summary": "Servidor MCP de comercio (Universal Commerce Protocol)", "description": "Endpoint JSON-RPC 2.0 que expone las herramientas de compra: `tools/list` las enumera y devuelve el esquema de cada una. Aquí se crean, actualizan y pagan los carritos. El perfil de descubrimiento está en `/.well-known/ucp`. Los importes van en unidades menores de la moneda: {\"amount\": 5990, \"currency\": \"EUR\"} son 59,90 €.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PeticionJsonRpc" }, "examples": { "listarHerramientas": { "summary": "Enumerar herramientas", "value": { "jsonrpc": "2.0", "id": 1, "method": "tools/list" } } } } } }, "responses": { "200": { "description": "Respuesta JSON-RPC 2.0.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RespuestaJsonRpc" } } } }, "429": { "$ref": "#/components/responses/Error" }, "default": { "$ref": "#/components/responses/Error" } } } } }, "components": { "securitySchemes": { "cuentaDeCliente": { "type": "oauth2", "description": "Solo para operaciones sobre la cuenta de un cliente. Los metadatos legibles por máquina están en https://molinetcalflari.com/.well-known/oauth-authorization-server (RFC 8414) y el recurso protegido en /.well-known/oauth-protected-resource (RFC 9728). Requiere PKCE con S256.", "flows": { "authorizationCode": { "authorizationUrl": "https://account.molinetcalflari.com/authentication/oauth/authorize", "tokenUrl": "https://account.molinetcalflari.com/authentication/oauth/token", "refreshUrl": "https://account.molinetcalflari.com/authentication/oauth/token", "scopes": { "openid": "Identificador del cliente autenticado.", "email": "Correo electrónico del cliente.", "customer-account-api:full": "Acceso completo a la API de cuenta de cliente: pedidos y datos propios.", "customer-account-mcp-api:full": "Acceso completo a la API MCP de cuenta de cliente." } } } } }, "responses": { "Error": { "description": "Error estructurado en JSON.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "productoNoEncontrado": { "summary": "Producto no publicado", "value": { "status": 404, "message": "Product not found", "description": "No product with id 1 is published in the online store" } }, "carritoInvalido": { "summary": "Variante inexistente", "value": { "status": 422, "message": "Cart Error", "description": "Cannot find variant" } } } } } } }, "schemas": { "Error": { "type": "object", "description": "Sobre de error de la tienda. El código HTTP y el campo status coinciden.", "required": [ "status", "message" ], "properties": { "status": { "type": "integer", "description": "Código HTTP repetido en el cuerpo.", "examples": [ 404, 422 ] }, "message": { "type": "string", "description": "Resumen corto y estable del error.", "examples": [ "Product not found", "Cart Error" ] }, "description": { "type": "string", "description": "Explicación con el detalle concreto: qué hacer o qué faltaba.", "examples": [ "Cannot find variant" ] } } }, "Producto": { "type": "object", "required": [ "id", "title", "handle", "variants" ], "properties": { "id": { "type": "integer", "format": "int64" }, "title": { "type": "string", "examples": [ "Garrafa 5 Litros - AOVE Arbequina Premium" ] }, "handle": { "type": "string", "description": "Identificador en la URL.", "examples": [ "aceite-arbequina-5l" ] }, "body_html": { "type": "string", "description": "Descripción en HTML." }, "vendor": { "type": "string", "examples": [ "El Molinet de cal Flari" ] }, "product_type": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } }, "published_at": { "type": "string", "format": "date-time" }, "variants": { "type": "array", "items": { "$ref": "#/components/schemas/Variante" } }, "images": { "type": "array", "items": { "type": "object", "properties": { "src": { "type": "string", "format": "uri" }, "width": { "type": "integer" }, "height": { "type": "integer" } } } } } }, "Variante": { "type": "object", "required": [ "id", "price", "available" ], "properties": { "id": { "type": "integer", "format": "int64", "description": "Identificador que se usa para añadir al carrito." }, "title": { "type": "string", "examples": [ "Default Title", "M" ] }, "price": { "type": "string", "description": "Precio decimal en euros, como cadena.", "examples": [ "59.90" ] }, "sku": { "type": [ "string", "null" ] }, "available": { "type": "boolean" }, "grams": { "type": "integer", "description": "Peso en gramos." } } }, "Coleccion": { "type": "object", "required": [ "id", "handle", "title" ], "properties": { "id": { "type": "integer", "format": "int64" }, "handle": { "type": "string", "examples": [ "catalogo", "aceites-arbequina" ] }, "title": { "type": "string" }, "description": { "type": "string" }, "products_count": { "type": "integer" } } }, "Carrito": { "type": "object", "required": [ "token", "item_count", "total_price", "currency" ], "properties": { "token": { "type": "string" }, "item_count": { "type": "integer" }, "total_price": { "type": "integer", "description": "Total en céntimos: 5990 son 59,90 €." }, "currency": { "type": "string", "examples": [ "EUR" ] }, "items": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } }, "PeticionJsonRpc": { "type": "object", "required": [ "jsonrpc", "method" ], "properties": { "jsonrpc": { "type": "string", "const": "2.0" }, "id": { "type": [ "integer", "string" ] }, "method": { "type": "string", "examples": [ "tools/list", "tools/call" ] }, "params": { "type": "object", "additionalProperties": true } } }, "RespuestaJsonRpc": { "type": "object", "required": [ "jsonrpc" ], "properties": { "jsonrpc": { "type": "string", "const": "2.0" }, "id": { "type": [ "integer", "string", "null" ] }, "result": { "type": "object", "additionalProperties": true }, "error": { "type": "object", "properties": { "code": { "type": "integer" }, "message": { "type": "string" }, "data": { "type": "object", "additionalProperties": true } } } } } } } }