How to Set Up Wholesale Pricing in WooCommerce (Without Breaking Checkout)
"Wholesale pricing" sounds like one thing. In reality it's five separate rules stacked on top of each other:
- Who gets the wholesale price?
- What is the wholesale price?
- Does the discount scale with quantity?
- What do retail customers see?
- What does the cart show at checkout?
Get any one wrong and you end up with retail customers buying at wholesale rates, wholesale customers seeing the MSRP, or a checkout that applies the wrong discount after taxes. This guide is the full setup, in order, with the gotchas.
Before you start: decide your roles
Wholesale pricing is always tied to a user role. Before you configure a single product, decide what roles you need:
wholesale_customer— a single tier, flat discountwholesale_tier_1,wholesale_tier_2,wholesale_tier_3— multiple tiers with different discountsdistributor,reseller,dealer— by relationship type
Most stores start with one and end up with three. Plan accordingly.
add_action( 'init', function () {
add_role( 'wholesale_customer', 'Wholesale Customer', [ 'read' => true ] );
});
Or use User Role Editor if you prefer UI. Roles aren't tied to WooCommerce — they're WordPress roles, which means you can filter on them anywhere (pricing, shipping, payment, content visibility, menus).
The four pricing patterns
Pattern 1: Flat percentage off for a role
Simplest model. Wholesale customers get 20% off everything. One rule to maintain.
add_filter( 'woocommerce_product_get_price', function ( $price, $product ) {
$user = wp_get_current_user();
if ( in_array( 'wholesale_customer', (array) $user->roles, true ) ) {
return (float) $price * 0.8;
}
return $price;
}, 10, 2 );
// Repeat for variation prices
add_filter( 'woocommerce_product_variation_get_price', /* same logic */, 10, 2 );
Pros: trivial to maintain. One knob (the 0.8). Cons: doesn't let you vary discount per product, per category, or per brand. If your margin on product A is 50% but on product B is 10%, a flat 20% off breaks your P&L on B.
Pattern 2: Per-product wholesale price
Every product has a second "wholesale price" field. The retail price shows to logged-out users and retail customers; the wholesale price shows to wholesale roles.
This is what most serious B2B stores end up on. It requires either a plugin (WooCommerce Wholesale Prices, B2BKing, Addify) or custom meta fields per product.
If you roll your own:
// Save wholesale price meta from product edit screen
add_action( 'woocommerce_process_product_meta', function ( $post_id ) {
if ( isset( $_POST['wholesale_price'] ) ) {
update_post_meta( $post_id, '_wholesale_price', wc_clean( $_POST['wholesale_price'] ) );
}
});
// Apply at cart
add_filter( 'woocommerce_product_get_price', function ( $price, $product ) {
$user = wp_get_current_user();
if ( in_array( 'wholesale_customer', (array) $user->roles, true ) ) {
$wholesale = get_post_meta( $product->get_id(), '_wholesale_price', true );
return $wholesale !== '' ? (float) $wholesale : $price;
}
return $price;
}, 10, 2 );
Pros: precise control. You can set each price to whatever the contract says. Cons: more to maintain. Adding a new product means remembering to set the wholesale price. Bulk editing helps.
Pattern 3: Tiered pricing (volume breaks)
"1-9 units: $60. 10-49 units: $55. 50+ units: $50."
Native WooCommerce has no tiered pricing. You either use a plugin (Dynamic Pricing & Discounts, Advanced Dynamic Pricing for WooCommerce) or wire it up in cart hooks.
Gotcha: tiered pricing compounds with role pricing. If a wholesale_customer gets 20% off AND buys 50 units, is the tier break "50 units at the wholesale price" or "50 units at the retail price minus 20%"? Decide this up front — it's the #1 source of bugs in wholesale pricing systems.
Pattern 4: Wholesale-only products
Some SKUs should only be visible and purchasable by wholesale customers. Think bulk packaging, case quantities, or distributor-exclusive products.
Use a custom taxonomy or a product meta flag + a query filter:
add_action( 'woocommerce_product_query', function ( $q ) {
$user = wp_get_current_user();
if ( ! in_array( 'wholesale_customer', (array) $user->roles, true ) ) {
$meta_query = (array) $q->get( 'meta_query' );
$meta_query[] = [
'key' => '_wholesale_only',
'compare' => 'NOT EXISTS',
];
$q->set( 'meta_query', $meta_query );
}
});
Plus block direct access to the single product page (in case someone has the URL):
add_action( 'template_redirect', function () {
if ( ! is_product() ) return;
global $post;
if ( get_post_meta( $post->ID, '_wholesale_only', true ) ) {
$user = wp_get_current_user();
if ( ! in_array( 'wholesale_customer', (array) $user->roles, true ) ) {
wp_safe_redirect( home_url() );
exit;
}
}
});
Hiding wholesale prices from retail customers
The most common bug in DIY wholesale pricing: the wholesale price shows up somewhere retail customers can see it. Places to check:
- Product archive pages (
/shop/) — price shown next to each product thumb - Product detail pages — both the summary price and the variation matrix
- Cart — in case a guest somehow has a wholesale item
- Mini-cart in the header
- Recently viewed / related products widgets
- REST API endpoints —
/wp-json/wc/v3/products/123returns prices publicly by default - XML sitemaps — some SEO plugins include price in product schema
- Google Shopping feed — if you're using a feed plugin, it may expose the lowest price
- JSON-LD product schema — emitted in
<head>by most themes, can includelowPrice/highPrice
If your wholesale price logic runs on woocommerce_product_get_price, all the on-page WooCommerce UI will get the right price based on the current user. But the REST API, sitemaps, and feeds run outside a user context — they see the "default" (retail) price, which is correct for them. The problem is if you're overriding the retail price to be the wholesale one (e.g., you set the wholesale price in _regular_price) — then retail pages will show it.
Rule of thumb: keep retail price in _regular_price and _price. Put wholesale in a separate meta field. Apply wholesale only at the filter level when the user's role matches.
The checkout rules you can't skip
Pricing is one half. The other half is what happens at checkout. Four rules that every wholesale system needs:
1. Minimum order quantity (MOQ)
"Wholesale orders require a $500 minimum" or "minimum 10 units per SKU."
Validate at the cart level, not at checkout:
add_action( 'woocommerce_check_cart_items', function () {
if ( ! in_array( 'wholesale_customer', wp_get_current_user()->roles ?? [], true ) ) return;
if ( WC()->cart->get_subtotal() < 500 ) {
wc_add_notice( 'Wholesale orders require a minimum of $500.', 'error' );
}
});
2. Role-based shipping
Wholesale orders go by freight, not USPS. Don't show the USPS option — it'll be picked by accident. See our role-based shipping guide.
3. Role-based payment
Wholesale customers with Net 30 terms pay via "Invoice on Account", not credit card. Credit card should be hidden for them. See role-based payment methods.
4. Tax logic
Wholesale customers are often tax-exempt (resellers with resale certificates). You need the is_vat_exempt flag set correctly. See WooCommerce Tax-Exempt Customers: The Complete Setup Guide.
Testing your pricing setup
Before launch, create one test user per role and do at least this:
- Log in as retail → browse shop → confirm retail prices everywhere
- Log in as wholesale tier 1 → browse shop → confirm tier 1 prices everywhere
- Log in as wholesale tier 2 → browse shop → confirm tier 2 prices
- Add items to cart → confirm cart subtotal matches expected
- Go to checkout → confirm no tax (if exempt), correct shipping options, correct payment options
- Place test order → confirm order total in admin matches cart
- Check order email → confirm prices in email match
Most bugs happen in step 6 or 7: the admin sees the order total computed one way, the customer email shows a different number, and the discrepancy gets blamed on "a plugin bug" when it's actually an interaction between two filters.
Also test the REST API if your site exposes it publicly:
curl https://yourstore.com/wp-json/wc/v3/products/123 | jq '.price, .regular_price'
This should return retail prices (the endpoint is public). If it returns wholesale prices, something's wrong with how you're storing them.
The plugin shortlist
If you don't want to build this from scratch (and you shouldn't unless you have a specific reason), here's the honest list:
| Plugin | Best for | Cost |
|---|---|---|
| B2BKing | All-in-one B2B — pricing, quotes, roles, registration | Mid-tier commercial |
| WooCommerce Wholesale Prices | Simple per-product wholesale pricing | Low commercial |
| Addify Role Based Pricing | Multiple roles, clean admin UI | Low commercial |
| Dynamic Pricing & Discounts | Tiered quantity pricing | Low commercial |
None of these handle tax exemption, role-based shipping/payment, or assisted ordering. That's where our plugins fit in the stack — they take care of the pieces the pricing plugins don't touch.
The short version
- Define your roles before you configure any product
- Pick one of four pricing patterns (flat %, per-product, tiered, wholesale-only)
- Keep retail prices in
_regular_price— put wholesale in a separate meta key - Check every surface where price leaks (archives, API, feeds, schema)
- Validate MOQ at cart, not checkout
- Test with real users logged in as each role, end-to-end, before launch
Wholesale pricing is 20% setup and 80% preventing the edge cases. Write the test checklist first; configure second.