# Create or update a whole product with productSet

> Sync a product from an external system in one call: productSet creates or updates the product, its options and variants, matched by handle.

Page: https://lintelagency.com/resources/graphql/product-set

## In short

To sync a product from an ERP or PIM, call productSet with the full product: title, status, productOptions and variants. Pass identifier with a handle or ID to update an existing product, or leave it out to create one. List fields are replaced, so variants you leave out are deleted. Use synchronous: true for small products and the asynchronous mode for large ones.

## Facts

- **API:** Admin GraphQL API
- **Operation:** productSet
- **Access scope:** write_products
- **Replaces (REST):** POST /admin/api/products.json and PUT /admin/api/products/{id}.json
- **API version:** 2026-10
- **Last checked:** 2 November 2026

## How to use it

1. Map your source record to ProductSetInput: every option with all its values, then one variant per combination with optionValues, sku and price.
2. Pass identifier with the handle (or the ID, or a unique metafield as customId) so a second run updates the product instead of creating a copy.
3. Send SyncProduct. With synchronous: true the product comes back in the response.
4. For large products, send synchronous: false and poll the productOperation query with the returned productSetOperation ID.

## Code

### product-set.graphql

```graphql
mutation SyncProduct($identifier: ProductSetIdentifiers, $input: ProductSetInput!) {
  productSet(identifier: $identifier, input: $input, synchronous: true) {
    product {
      id
      handle
      title
      status
      variants(first: 10) {
        nodes {
          id
          sku
          price
          selectedOptions {
            name
            value
          }
        }
      }
    }
    userErrors {
      code
      field
      message
    }
  }
}
```

### product-set.variables.json

```json
{
  "identifier": {
    "handle": "linen-shirt"
  },
  "input": {
    "title": "Linen shirt",
    "handle": "linen-shirt",
    "status": "ACTIVE",
    "vendor": "Your Brand",
    "productType": "Shirts",
    "tags": ["summer", "linen"],
    "productOptions": [
      {
        "name": "Size",
        "values": [{"name": "S"}, {"name": "M"}, {"name": "L"}]
      }
    ],
    "variants": [
      {"optionValues": [{"optionName": "Size", "name": "S"}], "sku": "LS-S", "price": "49.00"},
      {"optionValues": [{"optionName": "Size", "name": "M"}], "sku": "LS-M", "price": "49.00"},
      {"optionValues": [{"optionName": "Size", "name": "L"}], "sku": "LS-L", "price": "52.00"}
    ]
  }
}
```

### product-set.response.json

```json
{
  "data": {
    "productSet": {
      "product": {
        "id": "gid://shopify/Product/1234567890",
        "handle": "linen-shirt",
        "title": "Linen shirt",
        "status": "ACTIVE",
        "variants": {
          "nodes": [
            {"id": "gid://shopify/ProductVariant/1111111111", "sku": "LS-S", "price": "49.00", "selectedOptions": [{"name": "Size", "value": "S"}]},
            {"id": "gid://shopify/ProductVariant/1111111112", "sku": "LS-M", "price": "49.00", "selectedOptions": [{"name": "Size", "value": "M"}]},
            {"id": "gid://shopify/ProductVariant/1111111113", "sku": "LS-L", "price": "52.00", "selectedOptions": [{"name": "Size", "value": "L"}]}
          ]
        }
      },
      "userErrors": []
    }
  }
}
```

## Limits and edge cases

- List fields (variants, productOptions, metafields, collections, files) are replaced: entries you leave out are deleted. Other fields only change when you send them.
- Stores can have up to 2048 variants per product by default.
- Inside a bulk operation, productSet always runs synchronously.
- To change only variants or only options, use productVariantsBulkCreate, productVariantsBulkUpdate or the productOption mutations instead.

## Questions

### What is the difference between productSet and productCreate?

productCreate creates the product shell and options; variants follow in other calls. productSet takes the whole product with variants and either creates or updates it, which suits syncing from an external source.

### Will productSet delete variants?

Yes, if you leave them out. variants is a list field, so the result matches exactly the list you send.

### How do I upsert a product by handle?

Pass identifier: { handle: "your-handle" }. If no product has that handle, productSet creates one.

## Shopify documentation

- [productSet](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productSet)
- [Sync product data from an external source](https://shopify.dev/docs/apps/build/graphql/migrate/new-product-model/sync-data)

## Related

- [Import data with a staged upload and bulkOperationRunMutation](https://lintelagency.com/resources/graphql/bulk-import-with-staged-upload)
- [Create and update many variants at once](https://lintelagency.com/resources/graphql/bulk-create-update-variants)
- [Keeping inventory in sync with another system](https://lintelagency.com/journal/sync-inventory-external-system)

License: MIT. Free to use in your own store and in client work.
