> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtual.fit/llms.txt
> Use this file to discover all available pages before exploring further.

# Create or update a product

> Create a catalog product or progressively patch its current data.

Use a stable `product_id` in the URL. A new product needs `title` and at least
one image; later requests may send only changed fields.


## OpenAPI

````yaml openapi.yaml PUT /vfr/manage/sites/{site_id}/products/{product_id}
openapi: 3.1.0
info:
  title: virtual.fit API
  version: 2.0.0
  description: Catalog sync, storefront fit evidence, and product recommendation endpoints.
servers:
  - url: https://api.virtual.fit
security:
  - bearerAuth: []
tags:
  - name: Catalog
  - name: Storefront evidence
  - name: Recommendations
paths:
  /vfr/manage/sites/{site_id}/products/{product_id}:
    put:
      tags:
        - Catalog
      summary: Create or update a product
      description: >
        Creates a product or patches an existing product. A new product requires

        `title` and at least one item in `images`. Omitted fields stay
        unchanged.

        The current schema is version 2; omit `schema_version`.
      operationId: upsertCatalogProduct
      parameters:
        - $ref: '#/components/parameters/SiteID'
        - $ref: '#/components/parameters/ProductID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CatalogProductWrite'
            examples:
              essentials:
                summary: Fast initial product
                value:
                  title: Linen Midi Dress
                  handle: linen-midi-dress
                  images:
                    - url: https://cdn.example.com/linen-midi-dress-front.jpg
                      role: front
              inventory_update:
                summary: Update exact variant stock
                value:
                  variants:
                    - source_variant_id: linen-natural-m
                      available: false
                      inventory_quantity: 0
                      inventory_observed_at: '2026-08-01T09:30:00Z'
      responses:
        '200':
          description: Saved product and its canonical catalog representation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogProductWriteResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/TooLarge'
components:
  parameters:
    SiteID:
      name: site_id
      in: path
      required: true
      description: Custom JavaScript site ID from virtual.fit.
      schema:
        type: string
        format: uuid
    ProductID:
      name: product_id
      in: path
      required: true
      description: >
        Stable, case-sensitive identity that remains unchanged when the
        product's

        title, URL, options, variants, price, or media change. URL-encode it in

        the request path.
      schema:
        type: string
        maxLength: 255
  schemas:
    CatalogProductWrite:
      type: object
      description: |
        A new product requires `title` and `images`. For an existing product,
        every field is optional and omitted fields stay unchanged.
      properties:
        title:
          type: string
          maxLength: 500
          x-required-on-create: true
        images:
          type:
            - array
            - 'null'
          minItems: 1
          maxItems: 500
          x-required-on-create: true
          items:
            $ref: '#/components/schemas/CatalogImage'
        handle:
          type:
            - string
            - 'null'
          maxLength: 255
          description: Required when storefront URL resolution matches by handle.
        canonical_url:
          type: string
          format: uri
          maxLength: 8192
        enabled:
          type: boolean
          default: true
          description: >-
            Whether the product can use the fitting room. This is separate from
            sale availability.
        available:
          type:
            - boolean
            - 'null'
          description: >-
            Source-reported sale availability for a product without exact
            variants. Variant availability takes precedence when variants exist.
        rating:
          type:
            - number
            - 'null'
          minimum: 0
          maximum: 5
          description: Current overall customer rating.
        review_count:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Number of reviews represented by the overall rating.
        price:
          anyOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
        compare_at_price:
          anyOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
        variants:
          type:
            - array
            - 'null'
          maxItems: 5000
          items:
            $ref: '#/components/schemas/CatalogVariant'
        variants_complete:
          type: boolean
          default: false
          description: >-
            When true, omitted variants become inactive. Use with an empty array
            for a variantless product.
        options:
          type:
            - array
            - 'null'
          maxItems: 20
          description: Auto-derived from variant option values when omitted.
          items:
            $ref: '#/components/schemas/CatalogOption'
        size_charts:
          type:
            - array
            - 'null'
          maxItems: 50
          items:
            $ref: '#/components/schemas/CatalogSizeChart'
        garment_measurements:
          type:
            - array
            - 'null'
          maxItems: 250
          items:
            $ref: '#/components/schemas/CatalogGarmentMeasurement'
        model_observations:
          type:
            - array
            - 'null'
          maxItems: 50
          items:
            $ref: '#/components/schemas/CatalogModelObservation'
        brand:
          type:
            - string
            - 'null'
          maxLength: 255
        product_type:
          type:
            - string
            - 'null'
          maxLength: 255
        description:
          type:
            - string
            - 'null'
          maxLength: 65536
        custom_instructions:
          type:
            - string
            - 'null'
          maxLength: 4096
        prompt_tag:
          type:
            - string
            - 'null'
          maxLength: 255
          description: Concise garment naming override; auto-detected when omitted.
        source_active:
          type: boolean
        source_status_raw:
          type: string
          maxLength: 255
        source_updated_at:
          type: string
          format: date-time
        ingest:
          $ref: '#/components/schemas/CatalogIngest'
        schema_version:
          type: integer
          enum:
            - 1
            - 2
          default: 2
          description: >-
            Omit for version 2. Use 1 only for an existing image-only legacy
            integration.
    CatalogProductWriteResponse:
      type: object
      required:
        - product
        - catalog_product
        - schema_version
      properties:
        product:
          type: object
          description: Compatibility fitting-room product projection.
        catalog_product:
          type: object
          description: Persisted canonical catalog product.
        schema_version:
          type: integer
          const: 2
    CatalogImage:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          maxLength: 8192
          description: Public HTTP or HTTPS image URL.
        external_media_id:
          type: string
          maxLength: 255
          description: Stable source media ID that variants may reference.
        alt_text:
          type: string
        role:
          type: string
          maxLength: 255
          description: >-
            Prefer full-product `front`, `back`, or `side` views on a model or
            mannequin; `featured`, `primary`, and `gallery` are also accepted.
        position:
          type: integer
          default: 0
        width:
          type: integer
          minimum: 0
        height:
          type: integer
          minimum: 0
    Money:
      type: object
      required:
        - amount
        - currency
      properties:
        amount:
          type: string
          description: Decimal amount encoded as a string.
          examples:
            - '79.00'
        currency:
          type: string
          minLength: 3
          maxLength: 3
          description: Uppercase ISO 4217 currency code.
          examples:
            - USD
    CatalogVariant:
      type: object
      properties:
        source_variant_id:
          type: string
          maxLength: 255
          description: >-
            Stable source ID. Required unless `option_values` uniquely
            identifies the variant.
        option_values:
          type: object
          maxProperties: 20
          additionalProperties:
            type: string
            maxLength: 255
        title:
          type: string
          maxLength: 255
        sku:
          type: string
          maxLength: 255
        canonical_url:
          type: string
          format: uri
          maxLength: 8192
          description: Public URL that opens this exact variant.
        available:
          type:
            - boolean
            - 'null'
          description: >-
            Whether this exact variant can currently be purchased. Omit or use
            null when inventory is unknown.
        inventory_quantity:
          type:
            - integer
            - 'null'
          description: >-
            Exact source-reported count when known. Send `available` separately;
            quantity alone does not set availability.
        inventory_observed_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Source observation time. Older observations do not replace newer
            stock for the same variant; the request time is used when omitted.
        inventory_policy:
          type: string
          maxLength: 255
          description: Source inventory policy, preserved without normalization.
        inventory_item_id:
          type: string
          maxLength: 255
        price:
          $ref: '#/components/schemas/Money'
        compare_at_price:
          $ref: '#/components/schemas/Money'
        image_url:
          type: string
          format: uri
          maxLength: 8192
        media:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/CatalogVariantMedia'
      anyOf:
        - required:
            - source_variant_id
        - required:
            - option_values
    CatalogOption:
      type: object
      required:
        - key
        - values
      properties:
        key:
          type: string
          maxLength: 255
          description: Stable machine key shared with variant `option_values`.
        values:
          type: array
          maxItems: 1000
          items:
            $ref: '#/components/schemas/CatalogOptionValue'
        source_name:
          type: string
          maxLength: 255
          description: Merchant-facing label; defaults to `key`.
        kind:
          type: string
          enum:
            - visual
            - style
            - size
            - fit
            - length
            - width
            - other
          description: >-
            Auto-detected from the option name and variant evidence when
            omitted.
        presentation_role:
          type: string
          enum:
            - auto
            - include
            - exclude
          default: auto
        position:
          type: integer
          default: 0
        size_system_code:
          type: string
          enum:
            - us
            - eu
            - uk
            - au
            - jp
            - cn
            - kr
            - intl_alpha
            - inch
            - cm
            - mondopoint
            - brand_specific
            - one_size
            - unknown
        size_audience_code:
          type: string
          enum:
            - womens
            - mens
            - unisex
            - kids
            - youth
            - baby
            - unknown
        size_category_code:
          type: string
          enum:
            - apparel
            - footwear
            - denim
            - bra
            - hosiery
            - accessories
            - unknown
        source_size_system:
          type: string
          maxLength: 255
          description: Original source label when it does not map cleanly.
    CatalogSizeChart:
      type: object
      properties:
        url:
          type: string
          format: uri
          maxLength: 8192
          description: Public chart page or asset. A URL by itself is valid.
        external_id:
          type: string
          maxLength: 255
        content:
          type: object
          description: Optional structured chart data, limited to 256 KiB when encoded.
          additionalProperties: true
        applies_to:
          type: object
          description: >-
            Option selector such as `{"fit":"Petite"}`. Omit for a product-wide
            chart.
          additionalProperties:
            type: string
        locale:
          type: string
          description: BCP 47 locale such as `en-US`.
        size_system_code:
          type: string
          enum:
            - us
            - eu
            - uk
            - au
            - jp
            - cn
            - kr
            - intl_alpha
            - inch
            - cm
            - mondopoint
            - brand_specific
            - one_size
            - unknown
        size_audience_code:
          type: string
          enum:
            - womens
            - mens
            - unisex
            - kids
            - youth
            - baby
            - unknown
        size_category_code:
          type: string
          enum:
            - apparel
            - footwear
            - denim
            - bra
            - hosiery
            - accessories
            - unknown
      anyOf:
        - required:
            - url
        - required:
            - external_id
        - required:
            - content
    CatalogGarmentMeasurement:
      type: object
      required:
        - name
        - value
        - unit
      properties:
        name:
          type: string
          enum:
            - bust
            - chest
            - underbust
            - waist
            - hips
            - shoulder_width
            - sleeve_length
            - total_length
            - inseam
            - front_rise
            - back_rise
            - thigh
            - leg_opening
        value:
          type: number
          minimum: 0
        unit:
          type: string
          enum:
            - cm
            - in
        size_label:
          type: string
          maxLength: 255
        raw_text:
          type: string
          maxLength: 4096
        source:
          type: string
          maxLength: 255
        confidence:
          type: number
          minimum: 0
          maximum: 1
    CatalogModelObservation:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        size_worn:
          type: string
          maxLength: 255
        measurements:
          type: object
          maxProperties: 32
          additionalProperties:
            type: string
            maxLength: 255
          description: >-
            Source values with units, such as `{"height":"170 cm","waist":"68
            cm"}`.
        source:
          type: string
          maxLength: 255
        evidence:
          type: array
          maxItems: 32
          items:
            type: string
            maxLength: 4096
    CatalogIngest:
      type: object
      required:
        - event_id
        - run_id
        - sequence
        - producer
        - observed_at
      properties:
        event_id:
          type: string
          maxLength: 255
        run_id:
          type: string
          maxLength: 255
        sequence:
          type: integer
          minimum: 1
        producer:
          type: string
          maxLength: 255
        observed_at:
          type: string
          format: date-time
        observed_fields:
          type: array
          items:
            type: string
        completed_collections:
          type: array
          items:
            type: string
            enum:
              - images
              - options
              - variants
              - size_charts
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
    CatalogVariantMedia:
      type: object
      properties:
        external_media_id:
          type: string
          maxLength: 255
          description: ID of an item in product `images`.
        url:
          type: string
          format: uri
          maxLength: 8192
        role:
          type: string
          maxLength: 255
        position:
          type: integer
          default: 0
    CatalogOptionValue:
      type: object
      required:
        - source_value
      properties:
        source_value:
          type: string
          maxLength: 255
          description: Exact value used in variant `option_values`.
        display_label:
          type: string
          maxLength: 255
          description: Defaults to `source_value`.
        normalized_value:
          type: string
          maxLength: 255
        position:
          type: integer
          default: 0
        swatch:
          $ref: '#/components/schemas/CatalogSwatch'
    CatalogSwatch:
      type: object
      properties:
        label:
          type: string
          maxLength: 255
        color:
          type: string
          description: Primary CSS color.
        colors:
          type: array
          items:
            type: string
        image_url:
          type: string
          format: uri
          maxLength: 8192
          description: Public swatch image for a pattern or texture.
        raw_css:
          type: string
  responses:
    BadRequest:
      description: Invalid field or inconsistent catalog data.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: The API-key account cannot modify this site.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Site, product, variant, or recommendation context not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooLarge:
      description: Request body, file, field, or collection exceeds its documented limit.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Keep the virtual.fit API key on your server.

````