{
  "openapi": "3.1.0",
  "info": {
    "title": "Snuffelshop storefront API",
    "version": "2026-10-08",
    "summary": "Machine-readable map of the public JSON endpoints of snuffel-shop.nl.",
    "description": "Snuffelshop is a Dutch online shop for dog supplies, running on Shopify. This document describes the storefront endpoints an agent can call directly: the cart and search endpoints of Shopify's Ajax API, product recommendations, and the two Model Context Protocol (MCP) endpoints for store questions and for Universal Commerce Protocol (UCP) shopping.\n\nPrices are integers in euro cents (currency EUR). All responses are UTF-8 JSON.\n\nErrors: the Ajax endpoints answer with an `AjaxError` object (`status`, `message`, and a `description` that says how to fix the request). The MCP endpoints answer with a JSON-RPC 2.0 `error` object (`code`, `message`, optional `data`).\n\nCart endpoints act on the caller's own cart, which Shopify tracks with the `cart` cookie. Keep cookies between calls to build up one cart.\n\nThe human-readable agent guide is at https://snuffel-shop.nl/agents.md. Agents must never complete a payment without explicit approval from the buyer.",
    "contact": {
      "name": "Snuffelshop klantenservice",
      "url": "https://snuffel-shop.nl/pages/contact"
    },
    "termsOfService": "https://snuffel-shop.nl/policies/terms-of-service"
  },
  "externalDocs": {
    "description": "Agent guide for this shop",
    "url": "https://snuffel-shop.nl/agents.md"
  },
  "servers": [
    {
      "url": "https://snuffel-shop.nl",
      "description": "Production storefront"
    }
  ],
  "tags": [
    {
      "name": "Cart",
      "description": "Shopify Ajax cart API. Acts on the caller's own session cart.",
      "externalDocs": {
        "url": "https://shopify.dev/docs/api/ajax/reference/cart"
      }
    },
    {
      "name": "Catalog",
      "description": "Read-only product search and recommendations.",
      "externalDocs": {
        "url": "https://shopify.dev/docs/api/ajax/reference/predictive-search"
      }
    },
    {
      "name": "MCP",
      "description": "Model Context Protocol endpoints (JSON-RPC 2.0 over HTTP POST). Call the `tools/list` method first to get every tool with its input schema.",
      "externalDocs": {
        "url": "https://ucp.dev"
      }
    }
  ],
  "paths": {
    "/cart.json": {
      "get": {
        "tags": ["Cart"],
        "operationId": "getCart",
        "summary": "Get the current cart",
        "responses": {
          "200": {
            "description": "The caller's cart. An empty cart if none exists yet.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Cart" }
              }
            }
          }
        }
      }
    },
    "/cart/add.json": {
      "post": {
        "tags": ["Cart"],
        "operationId": "addToCart",
        "summary": "Add one or more product variants to the cart",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CartAddRequest" },
              "example": { "items": [{ "id": 51234567890123, "quantity": 1 }] }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The added line items.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/LineItem" }
                    }
                  },
                  "required": ["items"]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/CartError" }
        }
      }
    },
    "/cart/change.json": {
      "post": {
        "tags": ["Cart"],
        "operationId": "changeCartLine",
        "summary": "Set the quantity or properties of one cart line",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CartChangeRequest" },
              "example": { "line": 1, "quantity": 2 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The whole cart after the change.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Cart" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/CartError" }
        }
      }
    },
    "/cart/update.json": {
      "post": {
        "tags": ["Cart"],
        "operationId": "updateCart",
        "summary": "Set quantities for several variants, or the cart note and attributes",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CartUpdateRequest" },
              "example": { "updates": { "51234567890123": 2 } }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The whole cart after the update.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Cart" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/CartError" }
        }
      }
    },
    "/cart/clear.json": {
      "post": {
        "tags": ["Cart"],
        "operationId": "clearCart",
        "summary": "Remove every item from the cart",
        "description": "Send no request body.",
        "responses": {
          "200": {
            "description": "The emptied cart.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Cart" }
              }
            }
          },
          "422": { "$ref": "#/components/responses/CartError" }
        }
      }
    },
    "/search/suggest.json": {
      "get": {
        "tags": ["Catalog"],
        "operationId": "searchSuggest",
        "summary": "Search products, collections, pages and articles",
        "description": "Predictive search. Search terms are matched in Dutch; use Dutch product words (for example `hondenmand`, `riem`, `brokken`).",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The search terms.",
            "schema": { "type": "string", "minLength": 1 },
            "example": "hondenmand"
          },
          {
            "name": "resources",
            "in": "query",
            "required": false,
            "style": "deepObject",
            "explode": true,
            "description": "Which result types to return and how many, sent as `resources[type]=product&resources[limit]=5`.",
            "schema": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Comma-separated list of: product, collection, page, article, query.",
                  "default": "query,product,collection,page"
                },
                "limit": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 10,
                  "default": 10
                },
                "limit_scope": {
                  "type": "string",
                  "enum": ["all", "each"],
                  "default": "all"
                }
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching resources, grouped by type.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SearchSuggestResponse" }
              }
            }
          },
          "422": { "$ref": "#/components/responses/InvalidParameter" }
        }
      }
    },
    "/recommendations/products.json": {
      "get": {
        "tags": ["Catalog"],
        "operationId": "getProductRecommendations",
        "summary": "Products related to, or bought together with, a product",
        "parameters": [
          {
            "name": "product_id",
            "in": "query",
            "required": true,
            "description": "Numeric ID of a product published in the online store.",
            "schema": { "type": "integer", "format": "int64" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 10 }
          },
          {
            "name": "intent",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["related", "complementary"], "default": "related" }
          }
        ],
        "responses": {
          "200": {
            "description": "Recommended products.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "products": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Product" }
                    }
                  },
                  "required": ["products"]
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/InvalidParameter" }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "tags": ["MCP"],
        "operationId": "storefrontMcp",
        "summary": "Storefront MCP: ask about policies, shipping and products",
        "description": "JSON-RPC 2.0. Methods: `tools/list`, `tools/call`. Tool: `search_shop_policies_and_faqs`. No authentication.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/JsonRpcRequest" },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                  "name": "search_shop_policies_and_faqs",
                  "arguments": { "query": "Wat zijn de verzendkosten?" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/JsonRpcResult" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/ucp/mcp": {
      "post": {
        "tags": ["MCP"],
        "operationId": "ucpMcp",
        "summary": "UCP shopping MCP: catalog, cart, checkout and orders",
        "description": "JSON-RPC 2.0 transport of the Universal Commerce Protocol. Discover versions and capabilities first with `GET /.well-known/ucp`, then call `tools/list`.\n\nTools: `search_catalog`, `lookup_catalog`, `get_product`, `create_cart`, `get_cart`, `update_cart`, `cancel_cart`, `create_checkout`, `get_checkout`, `update_checkout`, `complete_checkout`, `cancel_checkout`, `get_order`.\n\nAccess is least-privilege: request only the OAuth scope the task needs. `dev.ucp.shopping.catalog.search:read` covers catalog reads; `dev.ucp.shopping.checkout:manage` covers creating and changing carts and checkouts. Reading an order also needs the buyer's `customer-account-mcp-api:full` grant. Listing tools needs no token. Shopify enforces which scope each call needs; a missing or invalid token returns HTTP 401 with a JSON-RPC error.\n\n`complete_checkout` takes payment: only call it after the buyer has explicitly approved the purchase.",
        "security": [
          {},
          { "shopifyCustomerAccount": ["dev.ucp.shopping.catalog.search:read"] },
          { "shopifyCustomerAccount": ["dev.ucp.shopping.checkout:manage"] },
          { "shopifyCustomerAccount": ["dev.ucp.shopping.checkout:manage", "customer-account-mcp-api:full"] }
        ],
        "parameters": [
          {
            "name": "UCP-Agent",
            "in": "header",
            "required": false,
            "description": "The agent's UCP profile, for example `profile=\"https://agent.example/.well-known/ucp\"`. Catalog and checkout tools fail with JSON-RPC error -32001 (`invalid_profile_url`) without it.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/JsonRpcRequest" },
              "example": { "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/JsonRpcResult" },
          "401": {
            "description": "The bearer token is missing a required scope, expired or invalid. Get a new token from the authorization server with the scope the tool needs.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JsonRpcErrorResponse" },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "error": { "code": -32000, "message": "AuthenticationFailed", "data": "Invalid global access token." }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "shopifyCustomerAccount": {
        "type": "oauth2",
        "description": "Shopify's authorization server for this shop, acting for a signed-in buyer. Metadata: https://snuffel-shop.nl/.well-known/oauth-authorization-server (RFC 8414) and https://snuffel-shop.nl/.well-known/oauth-protected-resource (RFC 9728). Use PKCE with S256. Send the token as `Authorization: Bearer <token>`.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://shopify.com/authentication/103433306495/oauth/authorize",
            "tokenUrl": "https://shopify.com/authentication/103433306495/oauth/token",
            "refreshUrl": "https://shopify.com/authentication/103433306495/oauth/token",
            "scopes": {
              "openid": "Sign the buyer in (OpenID Connect).",
              "email": "Read the signed-in buyer's email address.",
              "dev.ucp.shopping.catalog.search:read": "Search and read the product catalog through UCP.",
              "dev.ucp.shopping.checkout:manage": "Create, update, complete and cancel carts and checkouts through UCP.",
              "customer-account-mcp-api:full": "Act on the buyer's account through MCP, such as reading their orders.",
              "customer-account-api:full": "Full access to the buyer's account through the Customer Account API."
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body is not valid JSON. Send `Content-Type: application/json` with a JSON object."
      },
      "CartError": {
        "description": "The cart change was rejected, for example an unknown variant ID or more than the stock allows.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/AjaxError" },
            "example": { "status": 422, "message": "Cart Error", "description": "Cannot find variant" }
          }
        }
      },
      "InvalidParameter": {
        "description": "A required query parameter is missing or has an unsupported value.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/AjaxError" },
            "example": { "status": 422, "message": "Invalid parameter error", "description": "param is missing or the value is empty: q" }
          }
        }
      },
      "NotFound": {
        "description": "The referenced resource does not exist or is not published in the online store.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/AjaxError" },
            "example": { "status": 404, "message": "Product not found", "description": "No product with id 1 is published in the online store" }
          }
        }
      },
      "JsonRpcResult": {
        "description": "A JSON-RPC 2.0 response. Protocol and tool failures also use HTTP 200 and carry an `error` member instead of `result`.",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                { "$ref": "#/components/schemas/JsonRpcSuccessResponse" },
                { "$ref": "#/components/schemas/JsonRpcErrorResponse" }
              ]
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests from this IP address. Wait, then retry with exponential backoff."
      }
    },
    "schemas": {
      "AjaxError": {
        "type": "object",
        "description": "Error returned by the Shopify Ajax endpoints.",
        "properties": {
          "status": { "type": "integer", "description": "The HTTP status code, repeated." },
          "message": { "type": "string", "description": "Short error class." },
          "description": { "type": "string", "description": "What went wrong and how to fix the request." }
        },
        "required": ["status", "message", "description"]
      },
      "JsonRpcRequest": {
        "type": "object",
        "properties": {
          "jsonrpc": { "const": "2.0" },
          "id": { "type": ["integer", "string"] },
          "method": { "type": "string", "examples": ["tools/list", "tools/call"] },
          "params": { "type": "object" }
        },
        "required": ["jsonrpc", "id", "method"]
      },
      "JsonRpcSuccessResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": { "const": "2.0" },
          "id": { "type": ["integer", "string", "null"] },
          "result": {}
        },
        "required": ["jsonrpc", "id", "result"]
      },
      "JsonRpcErrorResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": { "const": "2.0" },
          "id": { "type": ["integer", "string", "null"] },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer",
                "description": "-32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32000 authentication failed, -32001 UCP discovery failed."
              },
              "message": { "type": "string" },
              "data": { "description": "Details and, where available, how to recover (for example a `continue_url` to hand the buyer)." }
            },
            "required": ["code", "message"]
          }
        },
        "required": ["jsonrpc", "id", "error"]
      },
      "CartAddRequest": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "integer", "format": "int64", "description": "Variant ID." },
                "quantity": { "type": "integer", "minimum": 1, "default": 1 },
                "properties": {
                  "type": "object",
                  "additionalProperties": { "type": "string" },
                  "description": "Line item properties, such as an engraving text."
                },
                "selling_plan": { "type": "integer", "format": "int64" }
              },
              "required": ["id"]
            }
          }
        },
        "required": ["items"]
      },
      "CartChangeRequest": {
        "type": "object",
        "description": "Identify the line by `line` (1-based position) or by `id` (variant ID or line item key).",
        "properties": {
          "line": { "type": "integer", "minimum": 1 },
          "id": { "type": ["integer", "string"] },
          "quantity": { "type": "integer", "minimum": 0, "description": "0 removes the line." },
          "properties": { "type": "object", "additionalProperties": { "type": "string" } }
        },
        "anyOf": [{ "required": ["line"] }, { "required": ["id"] }]
      },
      "CartUpdateRequest": {
        "type": "object",
        "properties": {
          "updates": {
            "description": "Variant ID → new quantity. 0 removes the variant.",
            "type": "object",
            "additionalProperties": { "type": "integer", "minimum": 0 }
          },
          "note": { "type": "string" },
          "attributes": { "type": "object", "additionalProperties": { "type": "string" } }
        }
      },
      "Cart": {
        "type": "object",
        "properties": {
          "token": { "type": "string" },
          "note": { "type": ["string", "null"] },
          "attributes": { "type": "object" },
          "item_count": { "type": "integer" },
          "total_price": { "type": "integer", "description": "In euro cents." },
          "original_total_price": { "type": "integer" },
          "total_discount": { "type": "integer" },
          "items_subtotal_price": { "type": "integer" },
          "requires_shipping": { "type": "boolean" },
          "currency": { "type": "string", "examples": ["EUR"] },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/LineItem" }
          }
        },
        "required": ["token", "item_count", "total_price", "currency", "items"]
      },
      "LineItem": {
        "type": "object",
        "properties": {
          "key": { "type": "string" },
          "id": { "type": "integer", "format": "int64", "description": "Variant ID." },
          "product_id": { "type": "integer", "format": "int64" },
          "variant_id": { "type": "integer", "format": "int64" },
          "title": { "type": "string" },
          "quantity": { "type": "integer" },
          "price": { "type": "integer", "description": "Unit price in euro cents." },
          "line_price": { "type": "integer" },
          "sku": { "type": ["string", "null"] },
          "handle": { "type": "string" },
          "url": { "type": "string" },
          "image": { "type": ["string", "null"] },
          "properties": { "type": ["object", "null"] }
        },
        "required": ["key", "id", "quantity", "price"]
      },
      "Product": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "format": "int64" },
          "title": { "type": "string" },
          "handle": { "type": "string" },
          "description": { "type": "string", "description": "HTML." },
          "vendor": { "type": "string" },
          "type": { "type": "string" },
          "price": { "type": "integer", "description": "Lowest variant price in euro cents." },
          "available": { "type": "boolean" },
          "url": { "type": "string" },
          "variants": { "type": "array", "items": { "type": "object" } }
        },
        "required": ["id", "title", "handle"]
      },
      "SearchSuggestResponse": {
        "type": "object",
        "properties": {
          "resources": {
            "type": "object",
            "properties": {
              "results": {
                "type": "object",
                "properties": {
                  "products": { "type": "array", "items": { "type": "object" } },
                  "collections": { "type": "array", "items": { "type": "object" } },
                  "pages": { "type": "array", "items": { "type": "object" } },
                  "articles": { "type": "array", "items": { "type": "object" } },
                  "queries": { "type": "array", "items": { "type": "object" } }
                }
              }
            },
            "required": ["results"]
          }
        },
        "required": ["resources"]
      }
    }
  }
}
