# Shopify compare_at_price to sale_price

Shopify and the feed specifications disagree about which field holds which number. Shopify sells at price and shows compare_at_price struck through; a feed expects price to be the regular price and sale_price the reduced one, so on a discounted product the two fields swap.

- **Canonical URL:** https://www.feedlab.io/knowledge/mappings/shopify-compare-at-price-to-sale-price
- **Last reviewed:** 18 September 2026
- **Source:** feedlab knowledge base
- **Licence:** free to quote and cite with attribution to feedlab


## The Shopify side

| Property | Value |
| --- | --- |
| API field | `variant.compare_at_price` |
| Called | Compare-at price |
| Found at | Product → Variants → edit variant → Pricing → Compare-at price |
| Feeds | `price`, `sale_price` |


## Before and after

| In Shopify | In the feed | Note |
| --- | --- | --- |
| `price 39.00, compare_at 49.00` | `price 49.00 GBP, sale_price 39.00 GBP` | Genuine discount. The fields swap so the channel can show the reduction. |
| `price 49.00, compare_at (empty)` | `price 49.00 GBP` | Not on sale. sale_price is omitted rather than duplicated. |
| `price 49.00, compare_at 49.00` | `price 49.00 GBP` | Left over from a finished sale. Equal values are not a discount. |
| `price 49.00, compare_at 39.00` | `price 49.00 GBP` | Compare-at below price is a data error, not a discount. Ignore it and send price alone. |
| `price 39.00, compare_at 49.00 (OpenAI)` | `price "49.00 GBP", sale_price "39.00 GBP"` | Same swap, with the amount and currency joined into one string for that channel. |



## The transformation, step by step

1. **Decide whether the variant is actually on sale** — A variant is discounted when compare_at_price is set and is higher than price. Shopify does not clear the field when a sale ends, so plenty of products carry a compare-at price equal to or below the selling price, and those are not on sale.
2. **Swap the fields when it is** — Send compare_at_price as the feed price and the Shopify price as sale_price. Mapping Shopify price straight to feed price on a discounted item means the channel shows the reduced price with no discount indicated, which loses the strikethrough that makes the offer visible.
3. **Send only price when it is not** — Omit sale_price entirely rather than repeating the same value in both fields. Equal values in price and sale_price read as a nil discount and some channels flag it.
4. **Format both consistently** — Two decimal places and the currency as the channel expects it, which is an amount plus an ISO code for most and a single string containing both for OpenAI. Both fields follow the same rule, and a sale price formatted differently from the price is a common oversight.
5. **Add an effective date only if the sale has one** — Where the channel supports a sale price effective date and you know the window, send it. Without it, the sale price applies until the next feed update, which is usually what you want for a discount managed in Shopify.



## What goes wrong

### The obvious mapping loses every discount

Mapping Shopify price to feed price is correct for full-price items and wrong for discounted ones. Nothing breaks, nothing is reported, and every sale in the catalogue simply stops being shown as a sale, which is the most expensive silent mapping error in a feed.

### Stale compare-at prices invent discounts

Shopify keeps compare_at_price after a sale ends. Treating any non-empty value as a discount advertises a reduction that does not exist, which is a landing-page mismatch and, in several markets, a pricing-claim problem rather than only a feed error.

### Discount apps never touch these fields

Automatic discounts, cart price rules and customer-specific pricing apply at checkout and are invisible in price and compare_at_price. A feed built from these two fields shows the undiscounted price, the landing page shows the discount, and the channel sees a mismatch it attributes to the feed.

### Tax and currency conventions differ per market

Whether the submitted price includes VAT depends on the market, not on Shopify. A multi-market store needs the tax treatment decided per feed, and the currency has to match the market rather than the store default.




## How feedlab maps it

feedlab compares the two values per variant rather than checking whether compare_at_price is set: a discount exists only when it is strictly greater than price, and then the fields swap. Otherwise price is sent alone and sale_price is omitted, and both are formatted to each channel's currency convention from the same rule.






## Frequently asked questions

### Which Shopify field is the sale price?

Shopify price is what the customer pays, so it becomes the feed sale_price on a discounted item, and compare_at_price becomes the feed price. The two swap relative to how Shopify names them.

### Why do my sale prices not show as discounts?

Because Shopify price was mapped to feed price. The channel then shows the reduced amount as the regular price, so there is nothing to strike through and no discount indicated.

### Should sale_price be sent when there is no sale?

No. Omit it rather than repeating the price, because identical values read as a zero discount and some channels flag it.

### Why do automatic discounts not appear in my feed?

They are applied at checkout and never written to price or compare_at_price, so nothing in the product data reflects them. That is also why the landing page and the feed disagree on those products.




## Primary sources

- [Google: sale_price attribute specification](https://support.google.com/merchants/answer/6324471)
- [Shopify: ProductVariant API reference](https://shopify.dev/docs/api/admin-graphql/latest/objects/ProductVariant)



