Skip to main content
The product endpoints cover the shape of a product. This page covers the behaviour behind four fields merchants ask about most.

original_price is a compare-at price

price is what the customer pays. original_price is the crossed-out price shown next to it, so shoppers can see the markdown.
It defaults to 0, which means no strike-through — the same as leaving the compare-at field empty in the admin. That is deliberate: a product with no promotion should show one price, not a price crossed out beside an identical price.
The storefront does not check that original_price is higher than price — it renders whatever you send. Setting it equal to price prints the same amount twice, once struck through, which reads to a shopper as a mistake. Send 0 when there is no promotion.
Variants carry their own price and original_price, and follow the same rule. When a product has variants, the storefront shows the first variant’s pair.
Products created through the API before September 2026 defaulted original_price to price, which showed exactly that duplicated, struck-through price. Send "original_price": 0 on those products to clear it.

description supports a small Markdown subset

Product descriptions are not full Markdown or HTML. The storefront renders this much: Line breaks are preserved as written. Everything else — headings, lists, tables, block quotes, code fences, raw HTML — is not parsed and appears literally, # and - characters included. A line starting with - reads as a bullet only because the dash is visible, not because a list is rendered.

images is the whole image list

images is an array of URLs in display order, and an update replaces the list:
Every image is copied onto Take App storage, because a supplier URL can rot or start blocking hotlinks while the storefront still has to render. A URL that is already ours is recognised and kept in place; any other URL is fetched and stored, and the response returns the new URL.
  • Omit images and the current images are left alone.
  • Send [] and all images are removed.
  • Send back the URLs the API returned to reorder without re-uploading. Sending an external URL again fetches it again.
  • Up to 10 images per request. A URL we can’t fetch fails the whole request with 400 invalid_request_error on images.

variants and options replace their list

Both are lists the product owns outright, so PATCH treats them the same way as images — the array you send becomes the list, in the order you send it.
  • An entry with id edits that variant. The id has to belong to this product.
  • An entry without id creates one.
  • A variant that already exists and is not in the array is deleted, along with its stock and any back-in-stock requests. Past orders keep their line items.
  • Omit the field entirely and the list is untouched. "variants": [] removes them all.
Options follow the same rules, except that an option’s type is fixed once created — it is only read on entries without an id. Changing inventory_quantity on an existing variant is recorded in stock history exactly as /api/v2/inventory_items would record it, so counts stay auditable however you set them.

Deleting a product

DELETE /api/v2/products/{product_id} removes the product, its variants, options and images, and drops it from every category.
Orders that already contain the product keep their line items, so revenue history stays intact. There is no undo — set "status": "HIDDEN" instead if you may want the product back.
Delete is REST-only. The MCP server exposes reads, creates and updates but not delete, so an AI agent connected to a store cannot remove a catalog.