Manage Products with the Admin API and JS SDK
In this guide, you'll learn how to create and manage products from a client application, such as a script or an external admin, using the JS SDK and the Admin API routes.
Authenticate Admin Requests#
All Admin API routes require an authenticated admin user. You can authenticate the JS SDK with a session, a JWT token, or a secret API key.
For server-side scripts, a secret API key is usually the simplest option:
import Medusa from "@medusajs/js-sdk"
export const sdk = new Medusa({
baseUrl: "http://localhost:9000",
apiKey: process.env.MEDUSA_SECRET_API_KEY,
})
Learn more about the authentication options in the JS SDK Authentication guide.
The rest of this guide uses the JS SDK methods and mentions the Admin API route that each method sends a request to.
Upload Product Images#
Product images are stored as URLs. So, before you create or update a product with new images, upload the image files using the sdk.admin.upload.create method. It sends a POST request to /admin/uploads as multipart/form-data.
For example:
const { files } = await sdk.admin.upload.create({
files: [
new File([imageBuffer], "shirt-front.png", {
type: "image/png",
}),
],
})
// each uploaded file has an `id` and a `url`
const imageUrls = files.map((file) => file.url)
The File Module Provider configured in your Medusa application stores the files and returns their URLs. You then pass these URLs in the product's images and thumbnail fields.
Create a Product#
To create a product, use the sdk.admin.product.create method. It sends a POST request to /admin/products.
For example, to create a draft product with one option, one variant, images, and associations to existing records:
const { product } = await sdk.admin.product.create({
title: "Medusa T-Shirt",
handle: "medusa-t-shirt",
description: "A comfortable cotton t-shirt.",
status: "draft",
images: imageUrls.map((url) => ({ url })),
thumbnail: imageUrls[0],
collection_id: "pcol_123",
type_id: "ptyp_123",
categories: [{ id: "pcat_123" }, { id: "pcat_456" }],
tags: [{ id: "ptag_123" }],
sales_channels: [{ id: "sc_123" }, { id: "sc_456" }],
shipping_profile_id: "sp_123",
options: [
{
title: "Size",
values: ["S", "M"],
},
],
variants: [
{
title: "Small",
sku: "SHIRT-S",
ean: "1234567890123",
manage_inventory: true,
allow_backorder: false,
options: {
Size: "S",
},
prices: [
{
currency_code: "usd",
amount: 20,
},
{
currency_code: "eur",
amount: 18,
},
],
},
],
metadata: {
source: "erp",
},
})
Where:
titleis the only required field.statusdefaults todraft. Other statuses areproposed,published, andrejected.categories,tags, andsales_channelsare arrays of objects with anidproperty. There's nocategory_idsfield.collection_idandtype_idare single IDs, since a product belongs to at most one collection and one type. A product can belong to many categories.imagesis an array of objects with aurlproperty. Medusa stores the images in the order of the array.- If you don't pass a
thumbnail, Medusa uses the URL of the first image as the thumbnail. - If you don't pass a
handle, Medusa generates it from thetitle. - Each variant requires a
titleand apricesarray. The keys of a variant'soptionsobject are option titles, and the values are one of that option'svalues. - Amounts in
pricesare the actual amounts, such as20for $20. Learn more in the Pricing Module's documentation. manage_inventorydefaults totrue. Learn more in the Product Variant Inventory guide.
Refer to the Create Product API reference for all accepted fields.
Retrieve a Product with Its Relations#
To retrieve a product, use the sdk.admin.product.retrieve method. It sends a request to the Get Product API route.
Use the fields query parameter to select the relations to retrieve. A * prefix retrieves all fields of a relation, and a + prefix adds a field to the default fields:
const { product } = await sdk.admin.product.retrieve(
"prod_123",
{
fields: "*categories,*images,*variants.prices,+metadata",
}
)
The same fields syntax works with sdk.admin.product.list, which sends a request to the List Products API route. For example, to retrieve each product's category IDs so you can find products without categories:
const { products } = await sdk.admin.product.list({
fields: "id,title,categories.id",
})
const uncategorized = products.filter(
(product) => !product.categories?.length
)
Learn more about the fields query parameter in the API reference.
Update a Product#
To update a product, use the sdk.admin.product.update method. It sends a POST request to the Update Product API route. Only the fields that you pass are updated.
For example, to update a product's details, images, and thumbnail:
const { product } = await sdk.admin.product.update(
"prod_123",
{
title: "Medusa T-Shirt",
description: "A soft cotton t-shirt.",
thumbnail: "https://example.com/new-front.png",
images: [
{ id: "img_123", url: "https://example.com/front.png" },
{ url: "https://example.com/new-back.png" },
],
categories: [{ id: "pcat_123" }],
tags: [{ id: "ptag_123" }],
metadata: {
color: "black",
},
}
)
Keep in mind the following about updates:
- Array fields such as
images,categories,tags, andsales_channelsreplace the product's existing values. For example, to add an image, pass the existing images along with the new one. - The order of the
imagesarray sets the order of the product's images. Pass an existing image'sidalong with itsurlto keep that image. - Updating
imagesdoesn't change thethumbnail. Passthumbnailto change it. metadatais merged with the product's existing metadata.- In the same request, you can update the product's
collection_id,type_id,shipping_profile_id,status, and other scalar fields.
To manage the product's variants and options, use the dedicated methods explained in the next sections.
options field. Use the sdk.admin.product.linkOptions method instead, which sends a request to the Add Options to Product API route.Manage Options#
To add options to a product or remove them from it, use the sdk.admin.product.linkOptions method. It sends a request to the Add Options to Product API route.
For example:
const { product } = await sdk.admin.product.linkOptions(
"prod_123",
{
add: [
"opt_123",
{
id: "opt_456",
value_ids: ["optval_1", "optval_2"],
},
],
remove: ["opt_789"],
}
)
The add array accepts one of the following for each item:
- The ID of an existing option.
- An object with the
idof an existing option and thevalue_idsof the option values to add to the product. Use this to add only some of an option's values. - An object with the
titleandvaluesof a new option to create.
The remove array accepts the IDs of options to remove from the product.
Remove an Option from a Product with Variants#
Since Medusa v2.22.0, you can remove an option from a product even when the product's variants use that option's values. Medusa removes the option's values from the product's variants. The variants themselves aren't deleted.
However, if removing the option's values makes two or more variants have the same combination of option values, Medusa throws an error and doesn't remove the option. For example, if a product has the options Size and Color, and has the variants "S / Blue" and "S / Red", removing the Color option fails because both variants would only have the value "S".
To remove the option in this case, first delete or update the variants that would conflict using the sdk.admin.product.batchVariants method explained in the next section.
Manage Variants and Prices#
To create, update, or delete multiple variants of a product in one request, use the sdk.admin.product.batchVariants method. It sends a request to the Manage Variants API Route.
For example:
const { created, updated, deleted } =
await sdk.admin.product.batchVariants("prod_123", {
create: [
{
title: "Medium",
sku: "SHIRT-M",
options: { Size: "M" },
prices: [{ currency_code: "usd", amount: 20 }],
},
],
update: [
{
id: "variant_123",
sku: "SHIRT-S-NEW",
options: { Size: "S" },
prices: [
{ id: "price_123", amount: 22 },
{ currency_code: "eur", amount: 19 },
],
},
],
delete: ["variant_456"],
})
Each item in update requires the variant's id. The other fields are optional.
When you pass prices for a variant, they replace the variant's existing prices:
- Pass the
idof an existing price to update it. - Pass a price without an
idto add it. - Existing prices that you don't pass are removed.
To update a single variant, you can also use the sdk.admin.product.updateVariant method, which sends a request to the Update Variant API route.
Assign Products to Collections and Categories in Bulk#
To add or remove multiple products in a collection, use the sdk.admin.productCollection.updateProducts method. It sends a request to the Manage Products of a Collection API route:
const { collection } =
await sdk.admin.productCollection.updateProducts(
"pcol_123",
{
add: ["prod_123", "prod_456"],
remove: ["prod_789"],
}
)
Since a product belongs to at most one collection, adding a product to a collection sets its collection_id to that collection.
To add or remove multiple products in a category, use the sdk.admin.productCategory.updateProducts method. It sends a request to the Manage Products of a Category API route:
const { product_category } =
await sdk.admin.productCategory.updateProducts(
"pcat_123",
{
add: ["prod_123", "prod_456"],
remove: ["prod_789"],
}
)
Adding a product to a category doesn't remove it from its other categories.
Publish a Product#
To publish a draft product, update its status to published using the Update Product API route.
Customers can view and purchase a published product in the sales channels that the product is available in.
Create, Update, or Delete Products in Bulk#
To create, update, and delete multiple products in one request, use the sdk.admin.product.batch method. It sends a request to the Manage Products API route:
const { created, updated, deleted } =
await sdk.admin.product.batch({
create: [
{
title: "Medusa Hoodie",
options: [{ title: "Size", values: ["M"] }],
variants: [
{
title: "Medium",
options: { Size: "M" },
prices: [{ currency_code: "usd", amount: 40 }],
},
],
},
],
update: [
{
id: "prod_123",
status: "published",
collection_id: "pcol_123",
categories: [{ id: "pcat_123" }],
},
],
delete: ["prod_456"],
})
Items in create accept the same fields as the create request. Items in update accept the same fields as the update request, along with the product's id.
To import many products from a CSV file, refer to the Import Products user guide instead.