Product & Variant Pricing — Canonical Model

Product & Variant Pricing — Canonical Model

Overview

EcartAPI exposes pricing for products and variants through three canonical fields — price, salePrice, and discount — with a single, platform-independent meaning. Regardless of how each source platform models pricing, the API normalizes it into the same contract, so you can consume pricing uniformly across all integrations. These fields are product price references only and do not affect orders.

This model applies to:

  • Product responses (GET /api/v2/products, GET /api/v2/products/{id})
  • Embedded variants (the variants array inside a product)
  • Standalone variant responses (GET /api/v2/products/{id}/variants, GET /api/v2/products/{id}/variants/{id})

The fields

FieldTypeMeaning
pricestring | nullThe original / list (regular) price reference.
salePricestring | nullThe discounted price reference. Always present when a price exists; equals price when there is no active promotion.
discountstring | nullThe monetary amount saved, in the product currency: price − salePrice. "0.00" when there is no promotion.

Invariants (always true)

  • discount === price − salePrice
  • salePrice <= price (a selling price above the list price is clamped to the list price, i.e. treated as "no promotion")
  • salePrice is always populated when price exists
  • discount is always a monetary amount — there is no percentage field, and there is no compareAtPrice field
  • If the source reports no price at all, then price, salePrice, and discount are all null. Values are never invented.

Examples

On promotion:

{
  "price": "100.00",
  "salePrice": "90.00",
  "discount": "10.00"
}

No promotion (salePrice mirrors price):

{
  "price": "100.00",
  "salePrice": "100.00",
  "discount": "0.00"
}

No price reported by the source:

{
  "price": null,
  "salePrice": null,
  "discount": null
}

Native discounts (amount or percentage)

Some platforms expose the discount directly instead of a second price. In that case the value is taken as reported and normalized into the canonical monetary discount:

  • A percentage is converted to a monetary amount relative to price.
  • The amount is clamped to the range [0, price].
  • When no explicit selling price is available, salePrice is derived as price − discount.

If a platform provides both an explicit selling price and a native discount, the selling price wins and discount is recomputed from the final prices, so the response is always internally consistent.


Per-platform mapping

Platforms that natively expose a promotion map both the list and the selling price:

Platformprice (list) sourcesalePrice (selling) sourceNotes
Shopifyvariant.compareAtPricevariant.priceList price fetched from Admin GraphQL in single-product, product-list and variants queries.
Tiendanubepricepromotional_priceApplied at product and variant level.
WooCommerceregular_price (or price)sale_priceFalls back to the price_html value when no numeric price is present.
Magento 2price_info.regular_priceprice_info.final_price
MercadoLibresale_price.regular_amount (fallback base_price / price)sale_price.amountRetrieved via /items/{id}/sale_price?context=channel_marketplace for single and list product reads.
Yampiprice_saleprice_discount
Olisterpprecos.precoprecos.preco_promocionaldiscount normalized to amount saved.
SheincurrentPrices[0].salePricecurrentPrices[0].specialPricediscount normalized to amount saved.
Traypricepromotional_pricediscount normalized to amount saved.

All other platforms: the source price is mapped to price, salePrice mirrors price, and discount is "0.00". A few platforms (PrestaShop 9, Liverpool, Lazada/Miravia) apply their own native promotion signals; where they do, they follow the same field meanings (discount = amount saved).


Shopify-specific improvements

  • List price exposed. The variant list price (compareAtPrice) is now requested from the Admin GraphQL API, so price returns the list price and salePrice the current selling price.
  • category.all shape normalized. products.category.all returns an array of { id, name } objects (matching other platforms), with segment names trimmed of leading/trailing whitespace. Previously it returned an array of strings.

Migration summary

GroupPlatformsWhat changedAction required
Meaning of price changesShopify, Tiendanubeprice now = list price reference (was the discounted price)Read salePrice for the discounted price reference
discount semantics fixedOlisterp, Shein, Traydiscount now = amount saved (was the promotional price)Read salePrice for the discounted price reference
category.all shapeShopifyproducts.category.all now = array of { id, name } (was array of strings)Update any parser expecting strings
AdditiveAll other platformssalePrice and discount are now always present (were often null)None required

FAQ

Do these fields affect orders?
No. They are product price references only and have no impact on order creation or order data.

Why not add a compareAtPrice field?
The model reuses the existing salePrice field for the discounted price and keeps price as the list price, avoiding a new field. There is no compareAtPrice in the response.

Is discount ever a percentage?
No. discount is always a monetary amount in the product currency. You can derive a percentage as discount / price.

What if a product has no price?
All three fields are null; the API does not fabricate values.


Released August 10, 2026. See the Changelog entry for a summary of breaking changes.


Did this page help you?