Shopify field mapping
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.
The Shopify side
- 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
Real values on the left as Shopify stores them, on the right as a channel needs them.
| 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
-
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.
-
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.
-
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.
-
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.
-
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.
Errors this mapping causes
Attributes involved
- price The price attribute is the item's full price before any discount, submitted as a number followed by an ISO 4217 currency code. It must match what a shopper sees on the landing page in that market, including whether tax is included, which is why multi-market Shopify stores so often trip the price mismatch disapproval.
- sale_price The sale_price attribute is the temporarily reduced price of an item, submitted alongside the full price so a channel can show the reduction rather than simply a lower number. On Shopify the reliable source is the variant's compare-at price relationship, and discounts applied by code or automatic rule are not visible in the product record at all.
Related concepts
Primary sources
Channel specifications change. These are the official documents this page is based on.