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

# Update a product

> Saves the product whole, as its editor does: send every field as it should be. `variants` is the complete list. An entry with an `id` changes that variant, an entry without one is added, and a variant left out is removed with its unsold stock. A variant buyers have received license keys from cannot be removed.

Read the product first and send it back changed. The fields that say "stay as they are" are the only ones that may be left out.

Needs the `products.write` permission (Change Products).



## OpenAPI

````yaml /api-reference/openapi.json put /stores/{storeId}/products/{productId}
openapi: 3.1.0
info:
  title: Shoply API
  version: '1'
  description: >-
    Everything a store's own server can do with a secret API key: its catalogue,
    orders, customers, marketing, finance and the rest of what the dashboard
    does. And the Customer API, which a buyer's own device asks with a token of
    theirs.
servers:
  - url: https://api.shoply.gg/v1
security:
  - apiKey: []
tags:
  - name: API key
  - name: Store
  - name: Domain
  - name: Payment methods
  - name: Analytics
  - name: Storefronts
  - name: Storefront code
  - name: Payment Routing
  - name: Custom payment methods
  - name: Products
  - name: Product pictures
  - name: Stock
  - name: Categories
  - name: Upsells
  - name: Subscriptions
  - name: Orders
  - name: Invoices
  - name: Reviews
  - name: Coupons
  - name: Coupon categories
  - name: Coupon pop-up
  - name: Sales
  - name: Blog
  - name: Search settings
  - name: Redirects
  - name: Sitemap
  - name: Cart Recovery
  - name: Email Marketing
  - name: Email Templates
  - name: Customers
  - name: Affiliates
  - name: Support Tickets
  - name: Blacklist
  - name: Fraud protection
  - name: Disputes
  - name: Finance
  - name: Tax
  - name: Expenses
  - name: Expense setup
  - name: API requests
  - name: Webhook endpoints
  - name: Webhook deliveries
  - name: Customer API
paths:
  /stores/{storeId}/products/{productId}:
    put:
      tags:
        - Products
      summary: Update a product
      description: >-
        Saves the product whole, as its editor does: send every field as it
        should be. `variants` is the complete list. An entry with an `id`
        changes that variant, an entry without one is added, and a variant left
        out is removed with its unsold stock. A variant buyers have received
        license keys from cannot be removed.


        Read the product first and send it back changed. The fields that say
        "stay as they are" are the only ones that may be left out.


        Needs the `products.write` permission (Change Products).
      operationId: products.update
      parameters:
        - name: storeId
          in: path
          required: true
          description: >-
            The store's ID. A key reaches the store it was made in: `GET /key`
            answers which.
          schema:
            type: string
        - name: productId
          in: path
          required: true
          description: The product.
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: The product's name.
                  example: Pro licence
                slug:
                  type: string
                  description: >-
                    The last part of its address on the storefront. Left out, it
                    is made from the name.
                description:
                  type: string
                  description: >-
                    What the product page says. Plain text with a few marks: a
                    line that starts with `#` is a heading, one that starts with
                    `-` is a point of a list, a blank line ends a paragraph, and
                    `**bold**` and backticks for code work inside a line.
                visibility:
                  type: string
                  description: >-
                    `public` is listed and can be bought. `unlisted` can be
                    bought by whoever has its link. `private` cannot be bought.
                  enum:
                    - public
                    - unlisted
                    - private
                  default: public
                variants:
                  type: array
                  description: >-
                    The product's options, 1 to 100 of them, in the order buyers
                    see them. A variant of the product that is not in this list
                    is removed.
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        description: >-
                          An existing variant's ID, to keep it and change it.
                          Left out, the entry is a new variant.
                      label:
                        type: string
                        description: >-
                          Its name, as a buyer picks it. No two variants of a
                          product share one.
                        example: 1 month
                      price:
                        type: number
                        description: >-
                          Its price in the store's currency, with at most 2
                          decimals.
                        example: 9.99
                      minQuantity:
                        type: integer
                        description: 'The fewest one order may hold: 1 to 100.'
                        default: 1
                      maxQuantity:
                        type: integer
                        description: >-
                          The most one order may hold: 1 to 100, or -1 for no
                          limit.
                        default: -1
                      deliveryMethods:
                        type: array
                        description: >-
                          What a buyer is sent when the order is paid. Any of
                          these together.
                        items:
                          type: string
                          enum:
                            - license_keys
                            - instructions
                            - dynamic
                            - files
                      stockMode:
                        type: integer
                        description: >-
                          How its stock is counted. `2`: it sells as many as it
                          has license keys in stock, which needs the
                          `license_keys` method. `3`: it sells down from a
                          number you set as `stock`. `1`: stock is not counted,
                          and it never sells out. Left out, an existing variant
                          keeps its own.
                        enum:
                          - 1
                          - 2
                          - 3
                        default: 2
                      stock:
                        type: integer
                        description: >-
                          For `stockMode` 3: how many are left, from 0 to
                          1,000,000. Each sale takes from it. Left out, the
                          number stays as it is, so a change to something else
                          does not undo what has sold since you read it. A
                          variant new to this way of counting starts at 0.
                      deliveryInstructions:
                        type: string
                        description: >-
                          Text sent to the buyer with their order, up to 5,000
                          characters. For the `instructions` method. The
                          product's own instructions, if it has some, are sent
                          before it.
                      dynamicUrl:
                        type: string
                        description: >-
                          The https address Shoply asks for the goods when the
                          order is paid. Needed for the `dynamic` method. It
                          cannot point at a private network.
                      customFieldsOn:
                        type: boolean
                        description: >-
                          Whether the buyer is asked `customFields` before they
                          pay. Left out, it stays as it is.
                      customFields:
                        type: array
                        description: >-
                          Questions the buyer answers before they pay, up to 10,
                          in the order they are asked. Kept while
                          `customFieldsOn` is false. The answers are on the
                          order, as `customFields`, and in the request to your
                          server for dynamic delivery. Left out, they stay as
                          they are.
                        items:
                          type: object
                          properties:
                            label:
                              type: string
                              description: >-
                                What the buyer reads over the box, up to 80
                                characters.
                              example: Discord Username
                            key:
                              type: string
                              description: >-
                                What the answer is called on the order and in
                                the request to your server: small letters,
                                digits and underscores, starting with a letter,
                                up to 40. No two of a variant share one. Left
                                out, it is made from the label.
                              example: discord_username
                            type:
                              type: string
                              description: >-
                                `text` is one line of up to 200 characters,
                                `long_text` up to 2,000, `number` a number,
                                `dropdown` and `choice` one of `options`, and
                                `tick` a box the buyer ticks.
                              enum:
                                - text
                                - long_text
                                - number
                                - dropdown
                                - choice
                                - tick
                              default: text
                            required:
                              type: boolean
                              description: >-
                                Whether the buyer can pay without answering. A
                                required `tick` has to be ticked.
                              default: false
                            placeholder:
                              type: string
                              description: >-
                                Shown in the box until something is typed, up to
                                120 characters. For `text`, `long_text` and
                                `number`.
                            hint:
                              type: string
                              description: A line under the box, up to 200 characters.
                            options:
                              type: array
                              description: >-
                                What the buyer picks from, 1 to 25, each up to
                                80 characters. For `dropdown` and `choice`.
                              items:
                                type: string
                          required:
                            - label
                      blockedPaymentMethods:
                        type: array
                        description: >-
                          Payment methods this variant cannot be bought with, by
                          their names in the payment methods list.
                        items:
                          type: string
                      sort:
                        type: integer
                        description: >-
                          Its place among the product's variants. Left out, its
                          place in the list sent.
                    required:
                      - label
                      - price
                deliveryInstructions:
                  type: string
                  description: >-
                    Text sent to the buyer with every variant of the product
                    once the order is paid, whatever the variant delivers, up to
                    5,000 characters. A variant's own instructions are sent
                    after it. Empty is none. Left out on an update, it stays as
                    it is.
                categoryIds:
                  type: array
                  description: >-
                    The categories it is in. Left out on an update, they stay as
                    they are.
                  items:
                    type: string
                isBestSeller:
                  type: boolean
                  description: Whether the storefront marks it as a best seller.
                  default: false
                badges:
                  type: array
                  description: >-
                    Up to 3 short labels on the product. Left out on an update,
                    they stay as they are.
                  items:
                    type: object
                    properties:
                      label:
                        type: string
                        description: What it says, up to 32 characters.
                      color:
                        type: string
                        description: >-
                          A preset, a colour of your own as `#rrggbb`, or empty
                          for the theme's neutral look.
                        enum:
                          - ''
                          - blue
                          - green
                          - amber
                          - red
                          - purple
                          - gray
                      kind:
                        type: string
                        description: >-
                          `status` also lists the product on the store's Status
                          page. One status a product.
                        enum:
                          - label
                          - status
                        default: label
                    required:
                      - label
                features:
                  type: array
                  description: >-
                    What the product does, as a list. Each entry is a feature on
                    its own or a named group of features. Up to 500 features and
                    30 groups. Left out on an update, they stay as they are.
                  items:
                    type: object
                    properties:
                      type:
                        type: string
                        description: A feature on its own, or a group.
                        enum:
                          - feature
                          - category
                      name:
                        type: string
                        description: >-
                          The feature, or the group's heading. Up to 100
                          characters.
                      features:
                        type: array
                        description: A group's features, as names.
                        items:
                          type: string
                    required:
                      - type
                      - name
                blockedPaymentMethods:
                  type: array
                  description: >-
                    Payment methods the product cannot be bought with, by their
                    names in the payment methods list. Left out on an update,
                    they stay as they are.
                  items:
                    type: string
                paymentFees:
                  type: object
                  description: >-
                    What a payment method adds to this product's price: a
                    payment method's name, and under it `percent` and `fixed`. A
                    fee can only add. Left out on an update, the fees stay as
                    they are.
                  example:
                    stripe:
                      percent: 3
                      fixed: 0.3
                salesWindow:
                  type: string
                  description: >-
                    The period the storefront's "sold" figure for this product
                    counts.
                  enum:
                    - all
                    - 1h
                    - 6h
                    - 12h
                    - 24h
                    - 7d
                    - 30d
                  default: all
                metadata:
                  type: object
                  description: >-
                    Anything of your own to keep with the product, as JSON.
                    Shoply stores it and does nothing with it. It is answered as
                    text.
                seoTitle:
                  type: string
                  description: >-
                    The product's own title in search results, up to 70
                    characters. Empty is its name.
                seoDescription:
                  type: string
                  description: >-
                    Its own description in search results, up to 200 characters.
                    Empty is its description.
                expectedUpdatedAt:
                  type: string
                  description: >-
                    The `updatedAt` you last read. Sent, the change is refused
                    with `stale_write` if the record has changed since. Left
                    out, it is saved whatever happened in between.
              required:
                - name
                - variants
      responses:
        '200':
          description: It worked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    nullable: true
                  code:
                    nullable: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      storeId:
                        type: string
                      name:
                        type: string
                      slug:
                        type: string
                      description:
                        type: string
                      seoTitle:
                        type: string
                      seoDescription:
                        type: string
                      image:
                        type: string
                      imageUrl:
                        nullable: true
                      visibility:
                        type: string
                      sort:
                        type: integer
                      isBestSeller:
                        type: boolean
                      badges:
                        type: array
                        items:
                          type: object
                          properties:
                            label:
                              type: string
                            color:
                              type: string
                            kind:
                              type: string
                      features:
                        type: array
                        items:
                          type: object
                          properties:
                            type:
                              type: string
                            name:
                              type: string
                      deliveryInstructions:
                        type: string
                      blockedPaymentMethods:
                        type:
                          - array
                          - object
                      paymentFees:
                        type:
                          - array
                          - object
                      showViewers:
                        type: boolean
                      showSales:
                        type: boolean
                      salesWindow:
                        type: string
                      saleNotifications:
                        type: boolean
                      metadata:
                        type: string
                      dateCreated:
                        type: string
                      updatedAt:
                        type: string
                      categoryIds:
                        type: array
                        items:
                          type: string
                      unitsSold:
                        type: integer
                      revenue:
                        type: integer
                      lowPrice:
                        type: number
                      highPrice:
                        type: integer
                      totalStock:
                        type: integer
                      untrackedVariants:
                        type: integer
                      imageCount:
                        type: integer
                      images:
                        type:
                          - array
                          - object
                      variants:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                            label:
                              type: string
                            price:
                              type: number
                            duration:
                              type: string
                            durationDays:
                              type: integer
                            stockMode:
                              type: integer
                            minQuantity:
                              type: integer
                            maxQuantity:
                              type: integer
                            stock:
                              type: integer
                            keyCount:
                              type: integer
                            deliveryMethods:
                              type: array
                              items:
                                type: string
                            deliveryType:
                              type: string
                            deliveryInstructions:
                              type: string
                            dynamicUrl:
                              type: string
                            customFieldsOn:
                              type: boolean
                            customFields:
                              type:
                                - array
                                - object
                            sort:
                              type: integer
                            blockedPaymentMethods:
                              type:
                                - array
                                - object
                      runningSubscriptions:
                        type: integer
              example:
                success: true
                message: null
                code: null
                data:
                  id: f463e265-097a-4235-bc2f-5239b19a9874
                  storeId: 743d82fb-6aad-44bb-ab00-d54c4b0eecb7
                  name: Northwind Pro
                  slug: northwind-pro
                  description: A licence for Northwind Pro, sent the moment you pay.
                  seoTitle: ''
                  seoDescription: ''
                  image: ''
                  imageUrl: null
                  visibility: public
                  sort: 0
                  isBestSeller: false
                  badges:
                    - label: New
                      color: green
                      kind: label
                  features:
                    - type: feature
                      name: Lifetime updates
                    - type: category
                      name: Support
                      features:
                        - Email
                        - Discord
                  deliveryInstructions: ''
                  blockedPaymentMethods: []
                  paymentFees: []
                  showViewers: false
                  showSales: false
                  salesWindow: all
                  saleNotifications: false
                  metadata: ''
                  dateCreated: '2026-10-22T16:20:18Z'
                  updatedAt: '2026-10-22T16:20:18.678Z'
                  categoryIds:
                    - e31dbb6f-c007-4e28-b0f8-49c4b432cfec
                  unitsSold: 0
                  revenue: 0
                  lowPrice: 9.99
                  highPrice: 79
                  totalStock: 0
                  untrackedVariants: 0
                  imageCount: 0
                  images: []
                  variants:
                    - id: 00e1d395-9ba2-4db3-900a-cb5e2811a2a9
                      label: 1 month
                      price: 9.99
                      duration: 1 month
                      durationDays: 30
                      stockMode: 2
                      minQuantity: 1
                      maxQuantity: -1
                      stock: 0
                      keyCount: 0
                      deliveryMethods:
                        - license_keys
                      deliveryType: license_keys
                      deliveryInstructions: ''
                      dynamicUrl: ''
                      customFieldsOn: false
                      customFields: []
                      sort: 0
                      blockedPaymentMethods: []
                    - id: defa3f9e-8aaf-423d-a0a6-fe65ab8043d4
                      label: 1 year
                      price: 79
                      duration: 1 year
                      durationDays: 365
                      stockMode: 2
                      minQuantity: 1
                      maxQuantity: -1
                      stock: 0
                      keyCount: 0
                      deliveryMethods:
                        - license_keys
                        - instructions
                      deliveryType: license_keys
                      deliveryInstructions: Paste the key under Help, Activate.
                      dynamicUrl: ''
                      customFieldsOn: false
                      customFields: []
                      sort: 1
                      blockedPaymentMethods: []
                  runningSubscriptions: 0
        '401':
          $ref: '#/components/responses/NoKey'
        '403':
          $ref: '#/components/responses/NotAllowed'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Refused'
        '429':
          $ref: '#/components/responses/TooMany'
      x-codeSamples:
        - lang: javascript
          label: Node.js
          source: |
            import Shoply from "@shoplygg/node";

            const shoply = new Shoply({ apiKey: process.env.SHOPLY_API_KEY });

            const result = await shoply.products.update("PRODUCT_ID", {
              name: "Pro licence",
              variants: [],
            });
        - lang: php
          label: PHP
          source: |
            <?php

            require 'vendor/autoload.php';

            $shoply = new Shoply\Client(['apiKey' => getenv('SHOPLY_API_KEY')]);

            $result = $shoply->products->update('PRODUCT_ID', [
                'name' => 'Pro licence',
                'variants' => [],
            ]);
        - lang: python
          label: Python
          source: >
            import shoply


            client = shoply.Client()  # reads SHOPLY_API_KEY


            result = client.products.update("PRODUCT_ID", name="Pro licence",
            variants=[])
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        A word of your own for this one change, up to 180 letters, digits, dots,
        dashes, underscores and colons. Sent again with the same key, the
        request is not carried out twice: the first answer comes back, with
        `Idempotent-Replay: true`.
      schema:
        type: string
  responses:
    NoKey:
      description: >-
        No API key was sent, or the one sent is not a live key:
        `api_key_required`, `invalid_api_key`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Refusal'
    NotAllowed:
      description: >-
        The key is real and may not do this: `missing_scope` (with
        `requiredScope`), `ip_not_allowed`, `account_unavailable`, or
        `dashboard_only` for something only a person in the dashboard may do.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Refusal'
    NotFound:
      description: >-
        There is no such thing in this store: `not_found`. A store that is not
        the key's own is answered the same way.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Refusal'
    Refused:
      description: >-
        The request was understood and not carried out. `validation_failed` says
        what is wrong with each field under `errors`; other codes are the
        route's own.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Refusal'
    TooMany:
      description: >-
        The key has used up its budget for now: `rate_limited`. `Retry-After`
        says how many seconds to wait.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Refusal'
  schemas:
    Refusal:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
          description: What is wrong, in words for a person.
        code:
          type: string
          description: What is wrong, as a word that does not change.
        data:
          nullable: true
        errors:
          type: object
          description: >-
            For `validation_failed`: what is wrong with each field, by the
            field's name.
          additionalProperties:
            type: string
      required:
        - success
        - message
        - code
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        A secret API key, made under Developers in the dashboard:
        `Authorization: Bearer shoply_sk_...`

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.