Wholesale pricing is rarely one rule. It is usually a base price per account group, a set of quantity breaks, and a handful of exceptions that the sales team has agreed over time. For years the default answer on Shopify was a pricing app, customer tags and some theme logic to hide the retail price. On Plus today, most of that can be native, with a small Shopify Function covering what the native model does not.
This is the pattern we reach for in wholesale distribution and B2B builds, and the reasoning behind each piece.
start with the native model
Shopify’s B2B features on Plus give you a few objects that map well onto how wholesale actually works:
- Companies and company locations. The buyer is a company with one or more locations, not a single customer record.
- Catalogs. Which products each location can see and buy.
- Price lists. Fixed prices or percentage adjustments for the products in a catalog.
- Quantity rules and volume pricing. Minimums, increments and price breaks by quantity.
- Payment terms. Net terms at the location level.
If your tiers are “gold accounts pay this, silver accounts pay that, and everyone gets a better price at higher quantities”, you may not need custom code at all. Configure a catalog and price list per tier, assign locations to the right catalog, and set volume pricing where it applies. The sales team can manage it in the admin, and it shows correctly everywhere Shopify renders a price.
We always try this first. Code you do not write is code you do not upgrade every quarter.
when does the native b2b model run out?
The native objects are good at static lists. They are less good at rules that combine several inputs at once. The ones we see most often:
- A discount that depends on the total quantity across a product family, not per line.
- A tier that depends on an account attribute your ERP owns, such as a trailing spend band.
- Mixed carts where some lines follow contract pricing and others follow the tier.
- Promotions for B2B buyers that must stack with, or exclude, their tier pricing in a specific way.
You can express some of these with enough price lists. Past a point, you end up with a pricing model nobody can audit. That is where a function earns its place.
the function, kept small
A Shopify Function for discounts runs inside checkout, reads a declared input query and returns the discounts to apply. It has no network access and strict resource limits, so the design rules are simple: keep the input narrow, keep the configuration outside the code, and keep the logic boring.
Our usual shape:
- Configuration in a metafield. Tier thresholds and percentages live in a JSON metafield on the discount or the shop. Changing a threshold is a settings change, not a deploy.
- Account data in metafields. Anything the rule needs about the buyer, such as a spend band synced from the ERP, is written to a company or customer metafield ahead of time. The function reads it; it never fetches it.
- One rule per function. A function that does tier pricing does only that. Promotions are a separate discount with clear combination settings.
// Sketch only: tiers come from a metafield, not constants.
export function run(input) {
const config = parseConfig(input.discountNode.metafield);
const familyQty = sumQuantity(input.cart.lines, config.family);
const tier = config.tiers.findLast((t) => familyQty >= t.minQty);
if (!tier) return NO_DISCOUNT;
return percentOffLines(input.cart.lines, config.family, tier.percent);
}
The code is short on purpose. The real work is in deciding which rules belong here and which belong in a price list, and in testing.
test with real carts
Pricing errors are expensive and quiet. A buyer who is overcharged may not notice for a month. One who is undercharged will rarely tell you.
Before cut-over we:
- Pull a sample of real past B2B orders and replay them on a dev store with the new setup.
- Compare every line price against what the buyer actually paid, and explain each difference.
- Test the edges: exactly on a threshold, one below, mixed carts, a buyer with no tier, a buyer with two locations.
- Run the largest realistic cart, because that is where function limits are tested.
Only when the differences are all intentional do we switch.
replacing a pricing app
If you are moving off a third-party pricing app, the order is:
- Export the rules you actually use from the app. It is usually fewer than you think.
- Map each rule to a price list, volume price or the function.
- Run both in parallel on a dev store and compare.
- Migrate accounts in groups, and keep the app installed but inactive until a full ordering cycle has passed.
The result is fewer moving parts: native objects the sales team can see and change, one small function with its configuration in data, and no script injecting prices into the theme.
when we would not do this
If your pricing is computed live from cost data, or every account has a negotiated contract per SKU that changes monthly, a function and price lists can still work, but the integration that feeds them becomes the real project. In that case we scope the sync first.
If you are weighing this up for a wholesale store, see how we approach custom apps and automation, or talk to an engineer.