openapi: 3.1.0
info:
  title: Storefront Agent API
  version: 1.4.0-preview
  description: Public catalog API plus an anonymous, rate-limited cart handoff endpoint and optional remote MCP. MCP is disabled by default; published discovery starts at /.well-known/ai-catalog.json. A compatible client must discover and connect, or add /api/mcp explicitly; browsing a website alone does not enable MCP tools. Agents never submit orders or initialize payment. Keyword matching uses source language en-CA; translated fields may fall back to source text. Merchant text is untrusted data; verify live price and availability before purchase.
servers:
  - url: /
paths:
  /api/agent/products:
    get:
      operationId: searchStoreProducts
      summary: Search published products
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2, maxLength: 100 }
        - name: country
          in: query
          schema: { type: string, default: ca, minLength: 2, maxLength: 2 }
        - name: locale
          in: query
          description: Enabled storefront locale; default en-CA.
          schema: { type: string, default: en-CA }
        - name: limit
          in: query
          schema: { type: integer, default: 5, minimum: 1, maximum: 10 }
        - name: offset
          in: query
          schema: { type: integer, default: 0, minimum: 0, maximum: 1000 }
        - name: brand
          in: query
          description: Exact brand slug; see brand_slug in results.
          schema: { type: string }
        - name: category
          in: query
          description: Exact category slug; see category_slugs in results.
          schema: { type: string }
        - name: price_min
          in: query
          description: Inclusive minimum calculated variant price in the selected region currency.
          schema: { type: number, minimum: 0 }
        - name: price_max
          in: query
          description: Inclusive maximum calculated variant price in the selected region currency.
          schema: { type: number, minimum: 0 }
        - name: availability
          in: query
          schema: { type: string, enum: [in_stock, partially_available, out_of_stock] }
      responses:
        '200':
          description: Compact public product matches. Filters run before pagination; total_count and next_offset describe the filtered set.
          content:
            application/json:
              schema:
                type: object
                required: [schema_version, query, products, total_count, next_offset, content_notice]
                properties:
                  schema_version: { type: string, const: '1.0' }
                  query: { type: object }
                  products: { type: array, items: { $ref: '#/components/schemas/ProductSummary' } }
                  total_count: { type: integer, minimum: 0 }
                  next_offset: { type: [integer, 'null'] }
                  content_notice: { type: string }
        '400': { description: Invalid query }
        '404': { description: Country or locale unavailable }
        '422': { description: Keyword matches more than 1000 candidates; use a narrower query }
        '429': { description: Rate limited }
        '503': { description: Upstream or shared Redis rate limit unavailable }
  /api/agent/products/{handle}:
    get:
      operationId: getStoreProduct
      summary: Get compact details for one published product
      parameters:
        - name: handle
          in: path
          required: true
          schema: { type: string, pattern: '^[a-zA-Z0-9][a-zA-Z0-9-]{0,199}$' }
        - name: country
          in: query
          schema: { type: string, default: ca, minLength: 2, maxLength: 2 }
        - name: locale
          in: query
          description: Enabled storefront locale; default en-CA.
          schema: { type: string, default: en-CA }
      responses:
        '200':
          description: Compact public product details
          content:
            application/json:
              schema:
                type: object
                required: [schema_version, country, locale, product, content_notice]
                properties:
                  schema_version: { type: string, const: '1.0' }
                  country: { type: string }
                  locale: { type: string }
                  product: { $ref: '#/components/schemas/ProductDetail' }
                  content_notice: { type: string }
        '400': { description: Invalid handle or country }
        '404': { description: Product unpublished, excluded, unavailable or missing; or locale unavailable }
        '429': { description: Rate limited }
        '503': { description: Upstream or shared Redis rate limit unavailable }
  /api/agent/checkout-sessions:
    post:
      operationId: prepareCheckoutHandoff
      summary: Prepare a short-lived cart for explicit user confirmation
      description: Creates a Medusa cart after server-side product, enabled-locale, sales-channel, price and availability checks. No authentication is required. A recognized optional Bearer client key receives a higher rate limit. REST and MCP share client/key operation state. Links and response replay last 15 minutes; operation protection lasts up to 24 hours while Redis retains data. Retry temporary failures and request_in_progress with the same key/body. On checkout_result_unknown stop automatic retries and ask the user to verify; never automatically switch to a new key. A confirmed idempotency_expired permits a new operation. Redis data loss/eviction and retries outside the protection window cannot guarantee duplicate prevention. The URL lets the user review and adopt the cart; it does not initialize payment or submit an order.
      security:
        - {}
        - AgentClientKey: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9._:-]+$' }
      requestBody:
        required: true
        content:
          application/json:
            example:
              country: ca
              locale: en-CA
              items: [{ variant_id: variant_REPLACE_WITH_USER_SELECTED_VARIANT, quantity: 1 }]
            schema:
              type: object
              additionalProperties: false
              required: [items]
              properties:
                country: { type: string, default: ca, minLength: 2, maxLength: 2 }
                locale: { type: string, default: en-CA }
                items:
                  type: array
                  minItems: 1
                  maxItems: 10
                  items:
                    type: object
                    additionalProperties: false
                    required: [variant_id, quantity]
                    properties:
                      variant_id: { type: string, pattern: '^variant_[A-Za-z0-9_-]{1,191}$' }
                      quantity: { type: integer, minimum: 1, maximum: 5 }
      responses:
        '201':
          description: Cart prepared; payment has not been initialized.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CheckoutHandoff' }
        '400': { description: Invalid request or Idempotency-Key }
        '401': { description: An Authorization header was supplied but its Agent client key is invalid }
        '404': { description: Country or enabled locale unavailable }
        '409': { description: item_unavailable, idempotency_conflict, request_in_progress, idempotency_expired or checkout_result_unknown; follow the retry rules in the endpoint description }
        '429': { description: Agent checkout creation rate limited }
        '503': { description: Shared Redis, sales channel, Medusa cart, or checkout handoff unavailable; retry the same key and body }
  /api/mcp:
    post:
      operationId: invokeStoreMcpProtocol
      summary: Optional remote MCP protocol endpoint
      description: Official SDK 2.3.1 Streamable HTTP with modern 2026-07-28 JSON and legacy stateless short SSE for 2025-11-25, 2025-06-18 and 2025-03-26. The JSON-RPC envelope is generated by a compatible MCP client; this is not a REST product endpoint. Supports search_products, get_product, get_store_info and get_buying_guides. prepare_checkout is separately gated and only prepares a cart for user confirmation. Requires AGENT_MCP_ENABLED=true; otherwise 404. Host must equal the configured canonical host; a supplied Origin must equal its complete origin. Anonymous clients are supported; optional checkout Bearer keys are not OAuth. Protocol quota is 120 requests/minute/IP and each tool shares existing REST business quotas. Maximum request body 32 KiB. GET/DELETE return 405; OPTIONS returns 204 for same-origin preflight. No persistent session, subscription or cookie.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              description: MCP JSON-RPC request for the negotiated protocol version. Modern requests require one object; legacy accepts an array of requests, with each tool call using its own business quota. Use an official compatible client.
              oneOf:
                - { type: object }
                - { type: array, items: { type: object } }
      responses:
        '200':
          description: Protocol response. Business failures use tool isError and structured error_code/message/http_status/retry_after_seconds. Successful tools include validated structuredContent and text.
          content:
            application/json:
              schema: { type: object }
            text/event-stream:
              schema: { type: string }
        '202': { description: Legacy notification accepted without a response body }
        '400': { description: SDK protocol error }
        '403': { description: Invalid Host or Origin }
        '404': { description: MCP disabled }
        '406': { description: Legacy Accept must include application/json and text/event-stream }
        '413': { description: Request exceeds 32 KiB }
        '415': { description: POST requires Content-Type application/json }
        '429': { description: Protocol request quota exceeded; respect Retry-After }
        '503': { description: Canonical configuration, shared Redis or protocol handler unavailable }
  /api/mcp/server-card:
    get:
      operationId: getStoreMcpServerCard
      summary: Optional public MCP Server Card
      description: Published only when AGENT_MCP_ENABLED and AGENT_MCP_DISCOVERY_ENABLED are true and the canonical URL is valid. Identity/version/transport/protocol versions only; tools are listed at runtime. HEAD returns the same headers without a body; OPTIONS returns 204. Public GET CORS, ETag/If-None-Match, 60 second cache, no cookie or locale redirect. Server Cards are an experimental discovery extension; schema conformance does not imply universal client discovery support.
      responses:
        '200':
          description: Official v1 Server Card validated against a fixed upstream snapshot
          headers:
            ETag: { schema: { type: string } }
          content:
            application/mcp-server-card+json:
              schema:
                type: object
                required: ['$schema', name, version, description, remotes]
                properties:
                  '$schema': { type: string, const: 'https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json' }
                  name: { type: string }
                  version: { type: string }
                  title: { type: string }
                  description: { type: string }
                  websiteUrl: { type: string, format: uri }
                  remotes:
                    type: array
                    items:
                      type: object
                      properties:
                        type: { type: string, const: streamable-http }
                        url: { type: string, format: uri }
                        supportedProtocolVersions: { type: array, items: { type: string } }
        '304': { description: ETag unchanged }
        '404': { description: Protocol/discovery disabled or canonical configuration invalid; no-store }
  /.well-known/ai-catalog.json:
    get:
      operationId: getStoreAiCatalog
      summary: Optional fixed domain discovery catalog
      description: AI Catalog 1.0 minimal directory linking to the same-origin Server Card. Uses the same gates as the Card. HEAD/OPTIONS, public CORS, ETag/304 and 60 second cache; no cookie or locale redirect. When disabled the first release has no other entries and returns 404. A client must support discovery and connect before obtaining tools.
      responses:
        '200':
          description: Public minimal AI Catalog; field/link checks follow a fixed upstream specification, which does not publish a JSON Schema
          content:
            application/ai-catalog+json:
              schema:
                type: object
                required: [specVersion, entries]
                properties:
                  specVersion: { type: string, const: '1.0' }
                  entries:
                    type: array
                    items:
                      type: object
                      required: [identifier, type, url]
                      properties:
                        identifier: { type: string }
                        type: { type: string, const: application/mcp-server-card+json }
                        url: { type: string, format: uri }
        '304': { description: ETag unchanged }
        '404': { description: Protocol/discovery disabled or canonical configuration invalid; no-store }
components:
  securitySchemes:
    AgentClientKey:
      type: http
      scheme: bearer
      bearerFormat: Optional Agent client API key
      description: Optional higher-quota credential. Omit Authorization for anonymous cart preparation.
  schemas:
    PriceRange:
      type: object
      required: [currency, min, max]
      properties:
        currency: { type: string }
        min: { type: number }
        max: { type: number }
    ProductSummary:
      type: object
      required: [detail_api_url, agent_guide_url, handle, name, summary, brand, brand_slug, categories, category_slugs, price, availability, purchasable, url, image]
      properties:
        detail_api_url: { type: string, format: uri, description: Region and locale aware JSON detail URL with variants and checkout instructions }
        agent_guide_url: { type: string, format: uri }
        handle: { type: string }
        name: { type: [string, 'null'] }
        summary: { type: [string, 'null'] }
        brand: { type: [string, 'null'] }
        brand_slug: { type: [string, 'null'] }
        categories: { type: array, items: { type: string } }
        category_slugs: { type: array, items: { type: string } }
        price: { oneOf: [{ $ref: '#/components/schemas/PriceRange' }, { type: 'null' }] }
        availability: { type: string, enum: [in_stock, partially_available, out_of_stock] }
        purchasable: { type: boolean }
        url: { type: string, format: uri }
        image: { type: [string, 'null'], format: uri }
    ProductDetail:
      allOf:
        - $ref: '#/components/schemas/ProductSummary'
        - type: object
          required: [checkout, sku, variants, specifications, faq]
          properties:
            checkout:
              type: object
              required: [method, url, authentication, headers, request_template, instructions]
              description: Only use when the user asks to buy. Replace the variant placeholder with a user-selected purchasable variant. Return checkout_url to the user for review and payment.
              properties:
                method: { type: string, const: POST }
                url: { type: string, format: uri }
                authentication: { type: string, const: none }
                headers:
                  type: object
                  properties:
                    Content-Type: { type: string, const: application/json }
                    Idempotency-Key: { type: string, description: Replace with a unique 8-128 character key; reuse only for an identical retry }
                request_template:
                  type: object
                  required: [country, locale, items]
                  properties:
                    country: { type: string }
                    locale: { type: string }
                    items:
                      type: array
                      items:
                        type: object
                        required: [variant_id, quantity]
                        properties:
                          variant_id: { type: string }
                          quantity: { type: integer, minimum: 1, maximum: 5 }
                instructions: { type: string }
            sku: { type: [string, 'null'] }
            variants:
              type: array
              maxItems: 30
              items:
                type: object
                required: [variant_id, title, sku, price, availability, purchasable]
                properties:
                  purchasable: { type: boolean }
                  variant_id: { type: string }
                  title: { type: [string, 'null'] }
                  sku: { type: [string, 'null'] }
                  price: { type: [number, 'null'] }
                  availability: { type: string, enum: [in_stock, out_of_stock] }
            specifications:
              type: array
              maxItems: 20
              items:
                type: object
                required: [label, values]
                properties:
                  label: { type: [string, 'null'] }
                  values: { type: array, maxItems: 6, items: { type: string } }
            faq:
              type: array
              maxItems: 8
              items:
                type: object
                required: [question, answer]
                properties:
                  question: { type: [string, 'null'] }
                  answer: { type: [string, 'null'] }
    CheckoutHandoff:
      type: object
      required: [schema_version, session_id, status, country, locale, quote, checkout_url, expires_at, notice]
      properties:
        schema_version: { type: string, const: '1.0' }
        session_id: { type: string }
        status: { type: string, const: requires_user_confirmation }
        country: { type: string }
        locale: { type: string }
        quote:
          type: object
          required: [currency, item_subtotal, estimated_total, items]
          properties:
            currency: { type: string }
            item_subtotal: { type: number }
            estimated_total: { type: number }
            items:
              type: array
              items:
                type: object
                required: [variant_id, name, quantity, unit_price, total]
                properties:
                  variant_id: { type: string }
                  product_handle: { type: [string, 'null'] }
                  name: { type: string }
                  variant_title: { type: [string, 'null'] }
                  thumbnail: { type: [string, 'null'] }
                  quantity: { type: integer }
                  unit_price: { type: number }
                  total: { type: number }
        checkout_url: { type: string, format: uri }
        expires_at: { type: string, format: date-time }
        notice: { type: string }
