Skip to content

Engineering

When the catalog price changes mid-negotiation: which price a quote should use

By Jahangir Alam · October 31, 2026 · 10 min read

Last verified
Shopify API
2026-10
Audience
Developers and agencies building or reviewing negotiation and quote approvals on a Shopify quote layer
Scope
Catalog price changes while a quote is open, at API 2026-10: where a Shopify price moves (variant price, price-list fixed prices and percentage adjustments, quantity breaks, catalog assignment, exchange rates) and which webhooks exist, four prices per line and their jobs, when to re-resolve the current price and when never to, resolving it with contextualPricing at the line's quantity with provenance and failure handling, showing the seller what moved, currency, and test cases

A catalog price that changes while a quote is open should never change the offer the buyer is holding. It should change what the seller sees and what the next seller version is checked against. That takes four prices per line, each with one job: the catalog price captured when the line entered the quote, the current price the buyer would pay today, the buyer's requested price, and the offered price. The offer is frozen once sent. The current price is re-resolved server-side at each seller send and stored with the version. A buyer's counter carries it forward unchanged, and nothing is re-priced after a version is sent. When the accepted quote becomes a Shopify draft order, the accepted price is sent explicitly.

This page is that design, with simplified code. Immutable quote versions covers why a sent version is never edited, and why a quote converts at the wrong price covers the draft-order side; this page is about the time in between, while the negotiation is open. Where QuotWay is the example, it is described as it works today; the rest are recommendations, labelled as such.

Shopify facts below were checked against Shopify's own pages on 31 October 2026 at API version 2026-10; sources are at the end.

Where a Shopify price can move

"The catalog price" is not one number on Shopify. A buyer's price is the result of several inputs, and each can change on its own:

What changes What it does to the buyer's price Does a webhook tell you?
The variant's price Moves the default price, and every price-list price set as a percentage adjustment products/update - which also fires when a product is ordered
A fixed price on a price list Moves that buyer's price for that variant only No topic names price lists
A price list's percentage adjustment Moves every variant on the list without a fixed price or volume pricing No topic names price lists
Deleting a fixed price Falls back to the list's percentage adjustment No topic names price lists
Volume pricing and quantity breaks Changes the price above a quantity threshold; once a product has volume pricing, the catalog's percentage adjustment no longer applies to it No topic names quantity breaks
Assigning another catalog to the buyer Can change the price: "the more specific price is used", and where neither catalog is more specific, the lower price Not documented
Market exchange rates Converted prices "change automatically with market exchange rates", unless you use manual rates or fixed international prices No

Two consequences shape the rest of the design. First, there is no event you can subscribe to that covers B2B price changes, so a quote layer can't keep its prices fresh by listening. It has to re-resolve the price at the moments that matter. Second, the variant's own price is the wrong number for a B2B buyer. The right number is the price after every adjustment, for that buyer, at that quantity.

Four prices, four jobs

Each line keeps four prices. They look redundant until you ask which decision each one serves:

Price Job Written Re-read
Captured catalog price History: what the buyer saw when they asked Once, when the line enters the quote Never
Current price (baseline) The decision baseline: discount %, floor checks, approval thresholds At each seller send, server-side At editor load (display), at each seller send
Requested price The buyer's ask When the buyer submits or counters Never - a counter is a new version
Offered price The offer When the seller sends a version Never - the next offer is a new version

The captured price and the current price are kept apart on purpose. Merging them loses either the history (what did the buyer see when they asked?) or the decision basis (how deep is this discount today?). A floor rule that compares the offer with a three-week-old catalog price is checking the wrong discount.

When to re-resolve, and when never to

The current price is read at a few defined moments, and each has a different purpose:

Moment Re-resolve the current price? Why
Intake - the buyer submits a request Record what the buyer saw; resolve server-side before any decision uses it The storefront can submit the price the buyer was shown, which is the right history. A price sent by a browser can't be checked against the catalog until the server resolves it, so it shouldn't become the decision baseline unchecked.
Editor load - the seller opens the quote Yes, for display The seller should see what moved before writing an offer. Nothing is stored.
Seller send - a new version goes out Yes, authoritatively, and store it on the version Floor and approval checks run on this number. Storing it means the decision can be explained later.
Buyer counter No - carry the version's baseline forward The buyer is answering a version. Re-pricing under them changes the question they answered.
Approval re-evaluation No - use the stored baseline The approver decides on the numbers that will go out, not on a newer price.
After a version is sent Never A sent version is a document. A changed price is information for the next seller version.
Conversion to a draft order No - force the accepted price Without an explicit price, Shopify prices the line at the current catalog price.

The last row is the one most often missed, and it's covered in depth in why a quote converts at the wrong price: for a variant line, priceOverride replaces the catalog price; the custom-line price field is ignored when a variant is set.

Resolving the current price with contextualPricing

Shopify's Admin API resolves a buyer's price in one field; what a Shopify B2B quote should start from walks through the chain of catalogs, price lists and quantity rules behind it. ProductVariant.contextualPricing takes a context - a companyLocationId for a B2B buyer, or a country for a market - and returns "the final price after all adjustments are applied", the quantity breaks, the quantity rule and an auditTrail recording "the adjustments used to determine the final price, in order of their application".

query CurrentPrices($ids: [ID!]!, $location: ID!) {
  nodes(ids: $ids) {
    ... on ProductVariant {
      id
      contextualPricing(context: { companyLocationId: $location }) {
        price { amount currencyCode }
        quantityPriceBreaks(first: 10) {
          nodes { minimumQuantity price { amount currencyCode } }
        }
        auditTrail { priceAdjustments { __typename } }
      }
    }
  }
}

Three details decide whether the number is right.

Resolve at the line's quantity. price is the price for one unit. A B2B line for 500 units may sit above a quantity break, and the buyer's real catalog price is the break price. Comparing an offer with the single-unit price makes every discount look deeper than it is, so floors and approvals fire on deals that don't need them. Breaks apply per variant and don't combine across variants. A sketch (recommendation):

// The buyer's catalog price at a given quantity: the highest break the quantity reaches,
// else the base contextual price.
function priceAtQuantity(cp: ContextualPricing, quantity: number): Money {
  const reached = cp.quantityPriceBreaks.nodes
    .filter((b) => b.minimumQuantity <= quantity)
    .sort((a, b) => b.minimumQuantity - a.minimumQuantity)[0];
  return reached ? reached.price : cp.price;
}

Keep the provenance. Store where the number came from beside it: the context used, the time and, if you can determine it, the catalog. "Discount 18% against 42.00" is only checkable if you can say what 42.00 was. The audit trail helps explain how a price was reached, but it names no catalog or price list, and it is empty when Shopify can't return a complete trail.

Decide what a failure means. Shopify can time out, or a scope can be missing. Silently skipping the floor check because the current price is unknown turns an outage into a pricing hole. Fall back to the last stored baseline and flag the line, or block the send for lines with no baseline at all (recommendation). Either is defensible; "no baseline, so no check" isn't.

Telling the seller what moved

Re-resolving at editor load is only useful if the seller sees the result. Show, per line, the captured price, the current price and the difference, before the seller writes an offer. Then the response is a decision, not an accident. A changed price leaves three honest options:

  • Honour the offer. The buyer is holding a sent version at the old price. Keep it until it expires; the expiry date is the price-protection window you chose. How long a quote should be valid is the decision about that window.
  • Re-quote. Send a new seller version at a new price, with the reason. The old version stays in the history, untouched.
  • Let it expire and start again from current prices.

Watch for the price going down. If the current price drops below an open offer, the buyer can see the lower price in your store. Flag that case as well as rises: an offer above the buyer's own catalog price is a trust problem, not only a margin question.

Currency: a rate is a price too

For a quote in the buyer's currency, the exchange rate is another input that moves. Shopify converts prices "automatically with market exchange rates" unless the store uses manual rates, and "international fixed prices will always take precedence over a manual conversion rate".

Two rules keep this manageable (recommendation). Fix the quote's currency when the quote is created, and never convert a sent offer. When the current price comes back in a different currency from the quote, convert it with a recorded rate - and record which rate, because "the price rose" and "the rate moved" lead to different conversations with the buyer. On the draft-order side, the exchange-rate section covers what Shopify does when a draft is created or edited in another currency.

What never moves

  • A sent version's prices. A catalog change after sending doesn't touch the offer.
  • Accepted lines. The acceptance names its version and records its prices.
  • The draft order's line prices. They are sent from the accepted version, explicitly, so the current catalog price can't replace them.

Immutable quote versions explains why the first two hold. Creating the draft order exactly once covers the third without duplicates.

Test cases for your own build

  1. Raise a variant's price after a quote is sent. The sent offer and the buyer's view don't change.
  2. Raise it, then send a new seller version. The new version stores the new current price, and the floor check uses it.
  3. Change a fixed price on a B2B price list. The editor shows the change on the next load, without any webhook.
  4. Change a price list's percentage adjustment. Every variant without a fixed price moves; variants with one don't.
  5. Quote 500 units of a variant with a break at 100. The baseline is the break price, not the single-unit price.
  6. Let the buyer counter after a price change. The counter carries the version's baseline; nothing is re-fetched.
  7. Make price resolution fail. The send either blocks or proceeds with a flagged, stored baseline - never with no floor check.
  8. Lower the price below an open offer. The seller sees the drop.
  9. Accept, then change the catalog price, then convert. The draft order carries the accepted price.

Where QuotWay fits

On company-aware quotes, QuotWay re-resolves the company-location price at every seller send, server-side, with contextualPricing and the quote's company location - not from the editor's copy. It stores the price on each version line, records the catalog it was resolved against on the version, and uses it as the baseline for price floors and discount approvals. A buyer's counter carries both the captured catalog price and that baseline forward without re-fetching either. The proposal editor shows each line's catalog reference and warns when a product isn't on the buyer's company-location catalog. A sent version is never re-priced, and converting an accepted quote sends the accepted price on every line of the Shopify draft order.

Company-aware quoting needs the Enterprise plan and a Shopify store with B2B; see Shopify B2B quoting, negotiation and pricing.

FAQ

Does a catalog price change affect a quote I've already sent?

It shouldn't. A sent version stores its own prices, so the offer the buyer is holding stays as sent. The change matters for the next seller version, whose floor and approval checks should use the current price, and for conversion, where the accepted price must be sent explicitly or Shopify uses the current catalog price.

Which price should discount approvals and price floors use?

The buyer's current price for that line, resolved server-side when the seller sends, at the line's quantity, and stored with the version. Not the price captured when the line entered the quote, which may be weeks old, and not a price supplied by the browser.

Is there a Shopify webhook for B2B price list changes?

Not one that names them. In API version 2026-10, the webhook topics include products/update, which fires when variants change (and when a product is ordered), but no topic names price lists, catalogs or quantity price breaks. Re-resolve prices at the moments a quote needs them instead of relying on an event.

How do I get a company location's price with the Admin API?

Query contextualPricing on the product variant with context: { companyLocationId: ... }. It returns the final price after all adjustments, the quantity price breaks, the quantity rule and an audit trail of the adjustments applied. Pick the break price for the line's quantity.

Should a buyer's counter-offer re-price against the current catalog?

No. A counter answers a specific version, so it carries that version's prices and baseline forward. Re-pricing under the buyer changes the question they answered. Pick up the new price when the seller sends the next version.

What if the catalog price goes down while a quote is open?

Flag it to the seller. An offer above the buyer's current catalog price is visible to the buyer in your store. The seller can re-quote at a lower price as a new version, or keep the offer and explain it; either way it should be a decision, not something found out at checkout.

Sources

Shopify pages, read on 31 October 2026 (API version 2026-10):

QuotWay behaviour is described from its public documentation and the app's source, read on 31 October 2026.

Related articles

See how QuotWay handles this on your store.

We’d like to set analytics cookies to understand how the site is used. They’re not required — declining changes nothing about how the site works, and you can change your mind any time on our privacy page.