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.

Looking for no-code docs? Refer to the Medusa Admin User Guide to learn how to manage products using the dashboard.

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:

Code
1import Medusa from "@medusajs/js-sdk"2
3export const sdk = new Medusa({4  baseUrl: "http://localhost:9000",5  apiKey: process.env.MEDUSA_SECRET_API_KEY,6})

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:

Code
1const { files } = await sdk.admin.upload.create({2  files: [3    new File([imageBuffer], "shirt-front.png", {4      type: "image/png",5    }),6  ],7})8
9// each uploaded file has an `id` and a `url`10const 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#

Note: Before creating a product with images, make sure to upload the images first.

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:

Code
1const { product } = await sdk.admin.product.create({2  title: "Medusa T-Shirt",3  handle: "medusa-t-shirt",4  description: "A comfortable cotton t-shirt.",5  status: "draft",6  images: imageUrls.map((url) => ({ url })),7  thumbnail: imageUrls[0],8  collection_id: "pcol_123",9  type_id: "ptyp_123",10  categories: [{ id: "pcat_123" }, { id: "pcat_456" }],11  tags: [{ id: "ptag_123" }],12  sales_channels: [{ id: "sc_123" }, { id: "sc_456" }],13  shipping_profile_id: "sp_123",14  options: [15    {16      title: "Size",17      values: ["S", "M"],18    },19  ],20  variants: [21    {22      title: "Small",23      sku: "SHIRT-S",24      ean: "1234567890123",25      manage_inventory: true,26      allow_backorder: false,27      options: {28        Size: "S",29      },30      prices: [31        {32          currency_code: "usd",33          amount: 20,34        },35        {36          currency_code: "eur",37          amount: 18,38        },39      ],40    },41  ],42  metadata: {43    source: "erp",44  },45})

Where:

  • title is the only required field.
  • status defaults to draft. Other statuses are proposed, published, and rejected.
  • categories, tags, and sales_channels are arrays of objects with an id property. There's no category_ids field.
  • collection_id and type_id are single IDs, since a product belongs to at most one collection and one type. A product can belong to many categories.
  • images is an array of objects with a url property. 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 the title.
  • Each variant requires a title and a prices array. The keys of a variant's options object are option titles, and the values are one of that option's values.
  • Amounts in prices are the actual amounts, such as 20 for $20. Learn more in the Pricing Module's documentation.
  • manage_inventory defaults to true. Learn more in the Product Variant Inventory guide.

Refer to the Create Product API reference for all accepted fields.

Tip: Create products as drafts, then publish them after you verify their details. Draft products aren't returned by the Store API routes.

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:

Code
1const { product } = await sdk.admin.product.retrieve(2  "prod_123",3  {4    fields: "*categories,*images,*variants.prices,+metadata",5  }6)

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:

Code
1const { products } = await sdk.admin.product.list({2  fields: "id,title,categories.id",3})4
5const uncategorized = products.filter(6  (product) => !product.categories?.length7)

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:

Code
1const { product } = await sdk.admin.product.update(2  "prod_123",3  {4    title: "Medusa T-Shirt",5    description: "A soft cotton t-shirt.",6    thumbnail: "https://example.com/new-front.png",7    images: [8      { id: "img_123", url: "https://example.com/front.png" },9      { url: "https://example.com/new-back.png" },10    ],11    categories: [{ id: "pcat_123" }],12    tags: [{ id: "ptag_123" }],13    metadata: {14      color: "black",15    },16  }17)

Keep in mind the following about updates:

  • Array fields such as images, categories, tags, and sales_channels replace the product's existing values. For example, to add an image, pass the existing images along with the new one.
  • The order of the images array sets the order of the product's images. Pass an existing image's id along with its url to keep that image.
  • Updating images doesn't change the thumbnail. Pass thumbnail to change it.
  • metadata is 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.

Note: Since Medusa v2.16.0, the update request doesn't accept an options field. Use the sdk.admin.product.linkOptions method instead, which sends a request to the Add Options to Product API route.

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:

Code
1const { created, updated, deleted } =2  await sdk.admin.product.batchVariants("prod_123", {3    create: [4      {5        title: "Medium",6        sku: "SHIRT-M",7        options: { Size: "M" },8        prices: [{ currency_code: "usd", amount: 20 }],9      },10    ],11    update: [12      {13        id: "variant_123",14        sku: "SHIRT-S-NEW",15        options: { Size: "S" },16        prices: [17          { id: "price_123", amount: 22 },18          { currency_code: "eur", amount: 19 },19        ],20      },21    ],22    delete: ["variant_456"],23  })

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 id of an existing price to update it.
  • Pass a price without an id to 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.

Tip: To set sale prices, create a price list instead of changing the variant's base prices.

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:

Code
1const { collection } =2  await sdk.admin.productCollection.updateProducts(3    "pcol_123",4    {5      add: ["prod_123", "prod_456"],6      remove: ["prod_789"],7    }8  )

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:

Code
1const { product_category } =2  await sdk.admin.productCategory.updateProducts(3    "pcat_123",4    {5      add: ["prod_123", "prod_456"],6      remove: ["prod_789"],7    }8  )

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.

Code
1await sdk.admin.product.update("prod_123", {2  status: "published",3})

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:

Code
1const { created, updated, deleted } =2  await sdk.admin.product.batch({3    create: [4      {5        title: "Medusa Hoodie",6        options: [{ title: "Size", values: ["M"] }],7        variants: [8          {9            title: "Medium",10            options: { Size: "M" },11            prices: [{ currency_code: "usd", amount: 40 }],12          },13        ],14      },15    ],16    update: [17      {18        id: "prod_123",19        status: "published",20        collection_id: "pcol_123",21        categories: [{ id: "pcat_123" }],22      },23    ],24    delete: ["prod_456"],25  })

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.

Was this page helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Docs MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break
⇧↵