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
variantsarray inside a product) - Standalone variant responses (
GET /api/v2/products/{id}/variants,GET /api/v2/products/{id}/variants/{id})
The fields
| Field | Type | Meaning |
|---|---|---|
price | string | null | The original / list (regular) price reference. |
salePrice | string | null | The discounted price reference. Always present when a price exists; equals price when there is no active promotion. |
discount | string | null | The monetary amount saved, in the product currency: price − salePrice. "0.00" when there is no promotion. |
Invariants (always true)
discount === price − salePricesalePrice <= price(a selling price above the list price is clamped to the list price, i.e. treated as "no promotion")salePriceis always populated whenpriceexistsdiscountis always a monetary amount — there is no percentage field, and there is nocompareAtPricefield- If the source reports no price at all, then
price,salePrice, anddiscountare allnull. 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,
salePriceis derived asprice − 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:
| Platform | price (list) source | salePrice (selling) source | Notes |
|---|---|---|---|
| Shopify | variant.compareAtPrice | variant.price | List price fetched from Admin GraphQL in single-product, product-list and variants queries. |
| Tiendanube | price | promotional_price | Applied at product and variant level. |
| WooCommerce | regular_price (or price) | sale_price | Falls back to the price_html value when no numeric price is present. |
| Magento 2 | price_info.regular_price | price_info.final_price | |
| MercadoLibre | sale_price.regular_amount (fallback base_price / price) | sale_price.amount | Retrieved via /items/{id}/sale_price?context=channel_marketplace for single and list product reads. |
| Yampi | price_sale | price_discount | |
| Olisterp | precos.preco | precos.preco_promocional | discount normalized to amount saved. |
| Shein | currentPrices[0].salePrice | currentPrices[0].specialPrice | discount normalized to amount saved. |
| Tray | price | promotional_price | discount 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, sopricereturns the list price andsalePricethe current selling price. category.allshape normalized.products.category.allreturns 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
| Group | Platforms | What changed | Action required |
|---|---|---|---|
Meaning of price changes | Shopify, Tiendanube | price now = list price reference (was the discounted price) | Read salePrice for the discounted price reference |
discount semantics fixed | Olisterp, Shein, Tray | discount now = amount saved (was the promotional price) | Read salePrice for the discounted price reference |
category.all shape | Shopify | products.category.all now = array of { id, name } (was array of strings) | Update any parser expecting strings |
| Additive | All other platforms | salePrice 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.
Updated 2 days ago