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.
- 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
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.
How to use it
- Map your source record to ProductSetInput: every option with all its values, then one variant per combination with optionValues, sku and price.
- 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.
- Send SyncProduct. With synchronous: true the product comes back in the response.
- For large products, send synchronous: false and poll the productOperation query with the returned productSetOperation ID.
Code
product-set.graphqlmutation 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{
"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{
"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": []
}
}
}
Written by Lintel. MIT license: free to use in your own store and in client work.
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
- Validated against the Admin or Storefront API schema for version 2026-10 on the date shown. Check the version in your request URL.
- IDs and store addresses in the examples are placeholders. Replace them with your own.
- Admin API calls need an access token from your app. Never put an Admin API token in a storefront or theme.
- MIT license. Use the code in your own store or in client work.
Need this built, tested and maintained for your store? See API integrations.
