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

# Create a product

> Creates a new product. Supports nested variants with prices and option types.

Option types and values are auto-created if they don't exist.
Prices are upserted by currency. Stock items are upserted by stock location.


**Required scope:** `write_products` (for API-key authentication).



## OpenAPI

````yaml /api-reference/admin.yaml post /api/v3/admin/products
openapi: 3.0.3
info:
  title: Admin API
  contact:
    name: Spree Commerce
    url: https://spreecommerce.org
    email: hello@spreecommerce.org
  description: >
    Spree Admin API v3 - Administrative API for managing products, orders, and
    store settings.


    ## Authentication


    The Admin API requires a secret API key passed in the `x-spree-api-key`
    header.

    Secret API keys can be generated in the Spree admin dashboard.


    ## Response Format


    All responses are JSON. List endpoints return paginated responses with
    `data` and `meta` keys.

    Single resource endpoints return a flat JSON object.


    ## Resource IDs


    Every resource is identified by an opaque string ID (e.g. `prod_86Rf07xd4z`,

    `variant_k5nR8xLq`, `or_UkLWZg9DAJ`). Use these IDs everywhere — URL paths,

    request bodies, and Ransack filters all accept them directly.


    ## Error Handling


    Errors return a consistent format:

    ```json

    {
      "error": {
        "code": "validation_error",
        "message": "Validation failed",
        "details": { "name": ["can't be blank"] }
      }
    }

    ```
  version: v3
servers:
  - url: http://{defaultHost}
    variables:
      defaultHost:
        default: localhost:3000
security: []
tags:
  - name: Authentication
    description: Admin user authentication
  - name: Product Catalog
    description: Products, variants, and option types
  - name: Orders
    description: >-
      Order management — orders, items, payments, fulfillments, refunds, gift
      cards, store credits
  - name: Customers
    description: Customer management — profiles, addresses, store credits, credit cards
  - name: Configuration
    description: Store configuration — payment methods, tag autocomplete
paths:
  /api/v3/admin/products:
    post:
      tags:
        - Product Catalog
      summary: Create a product
      description: >-
        Creates a new product. Supports nested variants with prices and option
        types.


        Option types and values are auto-created if they don't exist.

        Prices are upserted by currency. Stock items are upserted by stock
        location.



        **Required scope:** `write_products` (for API-key authentication).
      parameters:
        - name: x-spree-api-key
          in: header
          required: true
          schema:
            type: string
        - name: Authorization
          in: header
          required: true
          description: Bearer token for admin authentication
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: Premium T-Shirt
                description:
                  type: string
                slug:
                  type: string
                status:
                  type: string
                  enum:
                    - draft
                    - active
                    - archived
                tax_category_id:
                  type: string
                  description: Tax category ID
                category_ids:
                  type: array
                  items:
                    type: string
                  description: Array of category IDs
                tags:
                  type: array
                  items:
                    type: string
                  example:
                    - eco
                    - sale
                variants:
                  type: array
                  description: >-
                    Array of variant payloads. Variants can declare multiple
                    option pairs via `options:` and per-currency prices via
                    `prices:`. Stock counts go in `stock_items:` (per stock
                    location).
                  items:
                    type: object
                    properties:
                      sku:
                        type: string
                      options:
                        type: array
                        description: >-
                          One pair per option type the variant participates in
                          (e.g. size + color). Option types and values are
                          auto-created if missing.
                        items:
                          type: object
                          required:
                            - name
                            - value
                          properties:
                            name:
                              type: string
                              example: size
                            value:
                              type: string
                              example: Small
                      prices:
                        type: array
                        description: Per-currency prices. Upserted by currency.
                        items:
                          type: object
                          required:
                            - currency
                            - amount
                          properties:
                            currency:
                              type: string
                              example: USD
                            amount:
                              type: number
                              example: 29.99
                            compare_at_amount:
                              type: number
                              example: 39.99
                      stock_items:
                        type: array
                        description: >-
                          Per-stock-location inventory. Upserted by
                          stock_location_id.
                        items:
                          type: object
                          required:
                            - stock_location_id
                            - count_on_hand
                          properties:
                            stock_location_id:
                              type: string
                              description: Stock location ID (e.g. sloc_xxx)
                            count_on_hand:
                              type: integer
                              example: 50
                            backorderable:
                              type: boolean
              required:
                - name
      responses:
        '201':
          description: product created
          content:
            application/json:
              example:
                id: prod_gbHJdmfrXB
                name: New Product
                slug: new-product
                meta_title: null
                meta_description: null
                meta_keywords: null
                variant_count: 0
                available_on: null
                purchasable: false
                in_stock: false
                backorderable: false
                available: false
                description: null
                description_html: null
                default_variant_id: variant_gbHJdmfrXB
                thumbnail_url: null
                tags: []
                price: null
                original_price: null
                status: draft
                make_active_at: null
                discontinue_on: null
                metadata: {}
                deleted_at: null
                created_at: '2026-05-13T22:26:12.242Z'
                updated_at: '2026-05-13T22:26:12.245Z'
                tax_category_id: null
              schema:
                $ref: '#/components/schemas/Product'
        '422':
          description: validation error
          content:
            application/json:
              example:
                error:
                  code: validation_error
                  message: Name can't be blank
                  details:
                    name:
                      - can't be blank
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - api_key: []
          bearer_auth: []
      x-codeSamples:
        - lang: javascript
          label: Spree Admin SDK
          source: |-
            import { createAdminClient } from '@spree/admin-sdk'

            const client = createAdminClient({
              baseUrl: 'https://your-store.com',
              secretKey: 'sk_xxx',
            })

            const product = await client.products.create({
              name: 'Premium T-Shirt',
              description: 'Soft, organic cotton.',
              status: 'active',
              variants: [
                {
                  sku: 'TSHIRT-S-NAVY',
                  options: [
                    { name: 'size', value: 'Small' },
                    { name: 'color', value: 'navy' },
                  ],
                  prices: [
                    { currency: 'USD', amount: 29.99 },
                    { currency: 'EUR', amount: 27.99 },
                  ],
                  stock_items: [
                    { stock_location_id: 'sloc_UkLWZg9DAJ', count_on_hand: 50 },
                  ],
                },
                {
                  sku: 'TSHIRT-M-NAVY',
                  options: [
                    { name: 'size', value: 'Medium' },
                    { name: 'color', value: 'navy' },
                  ],
                  prices: [{ currency: 'USD', amount: 29.99 }],
                  stock_items: [
                    { stock_location_id: 'sloc_UkLWZg9DAJ', count_on_hand: 30 },
                  ],
                },
              ],
            })
components:
  schemas:
    Product:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        meta_title:
          type: string
          nullable: true
        meta_description:
          type: string
          nullable: true
        meta_keywords:
          type: string
          nullable: true
        variant_count:
          type: number
        available_on:
          type: string
          nullable: true
        purchasable:
          type: boolean
        in_stock:
          type: boolean
        backorderable:
          type: boolean
        available:
          type: boolean
        description:
          type: string
          nullable: true
        description_html:
          type: string
          nullable: true
        default_variant_id:
          type: string
        thumbnail_url:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        price:
          allOf:
            - $ref: '#/components/schemas/Price'
          nullable: true
        original_price:
          allOf:
            - $ref: '#/components/schemas/Price'
          nullable: true
        primary_media:
          $ref: '#/components/schemas/Media'
        media:
          type: array
          items:
            $ref: '#/components/schemas/Media'
        variants:
          type: array
          items:
            $ref: '#/components/schemas/Variant'
        default_variant:
          $ref: '#/components/schemas/Variant'
        option_types:
          type: array
          items:
            $ref: '#/components/schemas/OptionType'
        categories:
          type: array
          items:
            $ref: '#/components/schemas/Category'
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomField'
        prior_price:
          allOf:
            - $ref: '#/components/schemas/PriceHistory'
          nullable: true
        status:
          type: string
        make_active_at:
          type: string
          nullable: true
        discontinue_on:
          type: string
          nullable: true
        metadata:
          type: object
        deleted_at:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
        tax_category_id:
          type: string
          nullable: true
      required:
        - id
        - name
        - slug
        - meta_title
        - meta_description
        - meta_keywords
        - variant_count
        - available_on
        - purchasable
        - in_stock
        - backorderable
        - available
        - description
        - description_html
        - default_variant_id
        - thumbnail_url
        - tags
        - price
        - original_price
        - status
        - make_active_at
        - discontinue_on
        - metadata
        - deleted_at
        - created_at
        - updated_at
        - tax_category_id
      x-typelizer: true
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: record_not_found
            message:
              type: string
              example: Record not found
            details:
              type: object
              description: Field-specific validation errors
              nullable: true
              example:
                name:
                  - is too short
                  - is required
                email:
                  - is invalid
          required:
            - code
            - message
      required:
        - error
      example:
        error:
          code: validation_error
          message: Validation failed
          details:
            name:
              - is too short
            email:
              - is invalid
    Price:
      type: object
      properties:
        id:
          type: string
        amount:
          type: string
          nullable: true
        amount_in_cents:
          type: number
          nullable: true
        compare_at_amount:
          type: string
          nullable: true
        compare_at_amount_in_cents:
          type: number
          nullable: true
        currency:
          type: string
          nullable: true
        display_amount:
          type: string
          nullable: true
        display_compare_at_amount:
          type: string
          nullable: true
        price_list_id:
          type: string
          nullable: true
        variant_id:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
        variant:
          $ref: '#/components/schemas/Variant'
      required:
        - id
        - amount
        - amount_in_cents
        - compare_at_amount
        - compare_at_amount_in_cents
        - currency
        - display_amount
        - display_compare_at_amount
        - price_list_id
        - variant_id
        - created_at
        - updated_at
      x-typelizer: true
    Media:
      type: object
      properties:
        id:
          type: string
        product_id:
          type: string
          nullable: true
        variant_ids:
          type: array
          items:
            type: string
        position:
          type: number
        alt:
          type: string
          nullable: true
        media_type:
          type: string
        focal_point_x:
          type: number
          nullable: true
        focal_point_y:
          type: number
          nullable: true
        external_video_url:
          type: string
          nullable: true
        original_url:
          type: string
          nullable: true
        mini_url:
          type: string
          nullable: true
        small_url:
          type: string
          nullable: true
        medium_url:
          type: string
          nullable: true
        large_url:
          type: string
          nullable: true
        xlarge_url:
          type: string
          nullable: true
        og_image_url:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
        viewable_id:
          type: string
        download_url:
          type: string
          nullable: true
        metadata:
          type: object
        viewable_type:
          type: string
      required:
        - id
        - product_id
        - variant_ids
        - position
        - alt
        - media_type
        - focal_point_x
        - focal_point_y
        - external_video_url
        - original_url
        - mini_url
        - small_url
        - medium_url
        - large_url
        - xlarge_url
        - og_image_url
        - created_at
        - updated_at
        - viewable_id
        - download_url
        - metadata
        - viewable_type
      x-typelizer: true
    Variant:
      type: object
      properties:
        id:
          type: string
        product_id:
          type: string
        sku:
          type: string
          nullable: true
        options_text:
          type: string
        track_inventory:
          type: boolean
        media_count:
          type: number
        thumbnail_url:
          type: string
          nullable: true
        purchasable:
          type: boolean
        in_stock:
          type: boolean
        backorderable:
          type: boolean
        weight:
          type: number
          nullable: true
        height:
          type: number
          nullable: true
        width:
          type: number
          nullable: true
        depth:
          type: number
          nullable: true
        price:
          $ref: '#/components/schemas/Price'
        original_price:
          allOf:
            - $ref: '#/components/schemas/Price'
          nullable: true
        primary_media:
          $ref: '#/components/schemas/Media'
        media:
          type: array
          items:
            $ref: '#/components/schemas/Media'
        option_values:
          type: array
          items:
            $ref: '#/components/schemas/OptionValue'
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomField'
        prior_price:
          allOf:
            - $ref: '#/components/schemas/PriceHistory'
          nullable: true
        metadata:
          type: object
        position:
          type: number
        cost_price:
          type: string
          nullable: true
        cost_currency:
          type: string
          nullable: true
        barcode:
          type: string
          nullable: true
        weight_unit:
          type: string
          nullable: true
        dimensions_unit:
          type: string
          nullable: true
        deleted_at:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
        tax_category_id:
          type: string
          nullable: true
        available_stock:
          type: number
          nullable: true
        reserved_quantity:
          type: number
        total_on_hand:
          type: number
          nullable: true
        product_name:
          type: string
        prices:
          type: array
          items:
            $ref: '#/components/schemas/Price'
        stock_items:
          type: array
          items:
            $ref: '#/components/schemas/StockItem'
      required:
        - id
        - product_id
        - sku
        - options_text
        - track_inventory
        - media_count
        - thumbnail_url
        - purchasable
        - in_stock
        - backorderable
        - weight
        - height
        - width
        - depth
        - price
        - original_price
        - option_values
        - metadata
        - position
        - cost_price
        - cost_currency
        - barcode
        - weight_unit
        - dimensions_unit
        - deleted_at
        - created_at
        - updated_at
        - tax_category_id
        - available_stock
        - reserved_quantity
        - total_on_hand
        - product_name
      x-typelizer: true
    OptionType:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        label:
          type: string
        position:
          type: number
        kind:
          type: string
        metadata:
          type: object
        created_at:
          type: string
        updated_at:
          type: string
        option_values:
          type: array
          items:
            $ref: '#/components/schemas/OptionValue'
      required:
        - id
        - name
        - label
        - position
        - kind
        - metadata
        - created_at
        - updated_at
        - option_values
      x-typelizer: true
    Category:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        permalink:
          type: string
        position:
          type: number
        depth:
          type: number
        meta_title:
          type: string
          nullable: true
        meta_description:
          type: string
          nullable: true
        meta_keywords:
          type: string
          nullable: true
        children_count:
          type: number
        parent_id:
          type: string
          nullable: true
        description:
          type: string
        description_html:
          type: string
        image_url:
          type: string
          nullable: true
        square_image_url:
          type: string
          nullable: true
        is_root:
          type: boolean
        is_child:
          type: boolean
        is_leaf:
          type: boolean
        parent:
          $ref: '#/components/schemas/Category'
        children:
          type: array
          items:
            $ref: '#/components/schemas/Category'
        ancestors:
          type: array
          items:
            $ref: '#/components/schemas/Category'
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomField'
        metadata:
          type: object
        pretty_name:
          type: string
        lft:
          type: number
        rgt:
          type: number
        created_at:
          type: string
        updated_at:
          type: string
      required:
        - id
        - name
        - permalink
        - position
        - depth
        - meta_title
        - meta_description
        - meta_keywords
        - children_count
        - parent_id
        - description
        - description_html
        - image_url
        - square_image_url
        - is_root
        - is_child
        - is_leaf
        - metadata
        - pretty_name
        - lft
        - rgt
        - created_at
        - updated_at
      x-typelizer: true
    CustomField:
      type: object
      properties:
        id:
          type: string
        label:
          type: string
        type:
          type: string
          deprecated: true
        field_type:
          type: string
          enum:
            - short_text
            - long_text
            - rich_text
            - number
            - boolean
            - json
        key:
          type: string
        value:
          type: object
        created_at:
          type: string
        updated_at:
          type: string
        storefront_visible:
          type: boolean
        custom_field_definition_id:
          type: string
      required:
        - id
        - label
        - type
        - field_type
        - key
        - value
        - created_at
        - updated_at
        - storefront_visible
        - custom_field_definition_id
      x-typelizer: true
    PriceHistory:
      type: object
      properties:
        id:
          type: string
        amount:
          type: string
        amount_in_cents:
          type: number
        currency:
          type: string
        display_amount:
          type: string
        recorded_at:
          type: string
        variant_id:
          type: string
        price_id:
          type: string
        compare_at_amount:
          type: string
          nullable: true
        created_at:
          type: string
      required:
        - id
        - amount
        - amount_in_cents
        - currency
        - display_amount
        - recorded_at
        - variant_id
        - price_id
        - compare_at_amount
        - created_at
      x-typelizer: true
    OptionValue:
      type: object
      properties:
        id:
          type: string
        option_type_id:
          type: string
        name:
          type: string
        label:
          type: string
        position:
          type: number
        color_code:
          type: string
          nullable: true
        option_type_name:
          type: string
        option_type_label:
          type: string
        image_url:
          type: string
          nullable: true
        metadata:
          type: object
        created_at:
          type: string
        updated_at:
          type: string
        option_type:
          $ref: '#/components/schemas/OptionType'
      required:
        - id
        - option_type_id
        - name
        - label
        - position
        - color_code
        - option_type_name
        - option_type_label
        - image_url
        - metadata
        - created_at
        - updated_at
      x-typelizer: true
    StockItem:
      type: object
      properties:
        id:
          type: string
        count_on_hand:
          type: number
        backorderable:
          type: boolean
        stock_location_id:
          type: string
          nullable: true
        variant_id:
          type: string
          nullable: true
        metadata:
          type: object
        created_at:
          type: string
        updated_at:
          type: string
        allocated_count:
          type: number
        available_count:
          type: number
        stock_location:
          $ref: '#/components/schemas/StockLocation'
        variant:
          $ref: '#/components/schemas/Variant'
      required:
        - id
        - count_on_hand
        - backorderable
        - stock_location_id
        - variant_id
        - metadata
        - created_at
        - updated_at
        - allocated_count
        - available_count
      x-typelizer: true
    StockLocation:
      type: object
      properties:
        id:
          type: string
        state_abbr:
          type: string
          nullable: true
        name:
          type: string
        address1:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        zipcode:
          type: string
          nullable: true
        country_iso:
          type: string
          nullable: true
        country_name:
          type: string
          nullable: true
        state_text:
          type: string
          nullable: true
        admin_name:
          type: string
          nullable: true
        address2:
          type: string
          nullable: true
        state_name:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        company:
          type: string
          nullable: true
        active:
          type: boolean
        default:
          type: boolean
        backorderable_default:
          type: boolean
        propagate_all_variants:
          type: boolean
        kind:
          type: string
        pickup_enabled:
          type: boolean
        pickup_stock_policy:
          type: string
        pickup_ready_in_minutes:
          type: number
          nullable: true
        pickup_instructions:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
      required:
        - id
        - state_abbr
        - name
        - address1
        - city
        - zipcode
        - country_iso
        - country_name
        - state_text
        - admin_name
        - address2
        - state_name
        - phone
        - company
        - active
        - default
        - backorderable_default
        - propagate_all_variants
        - kind
        - pickup_enabled
        - pickup_stock_policy
        - pickup_ready_in_minutes
        - pickup_instructions
        - created_at
        - updated_at
      x-typelizer: true
  securitySchemes:
    api_key:
      type: apiKey
      name: x-spree-api-key
      in: header
      description: Secret API key for admin access
    bearer_auth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token for admin user authentication

````