🇹🇷 Türkçe: Bu yazının Türkçesini oku →

B2B ecommerce plays by different rules than B2C, and pricing is where the two worlds diverge hardest. Your wholesale customers expect negotiated pricing, volume discounts, net payment terms, and one-click reordering. Your ERP already manages every bit of that — it is the system your sales team trusts and your accountants reconcile against. So the challenge is not building a pricing engine in WooCommerce. It is surfacing the one you already own without quietly cloning its rules into a second place that will drift out of sync the first time someone edits a contract price.

I have watched teams get this wrong the expensive way: they rebuild tier logic in WooCommerce, it looks right in the demo, and six months later the storefront is quoting a price the ERP no longer honours. Here is the architecture I would actually ship, and the one decision that keeps it out of trouble.

The B2B Pricing Problem

In your ERP, pricing looks like this:

Customer: ABC Corp (Tier: Gold)

├── Product: Widget A

│ ├── List Price: $100

│ ├── Gold Tier Price: $75

│ ├── Volume 10+: $70

│ ├── Volume 50+: $65

│ └── Special Contract Price: $62 (until Dec 2026)

├── Product: Widget B

│ ├── List Price: $200

│ └── Gold Tier Price: $160

WooCommerce has none of this natively. It supports one regular price and one sale price per product — which is exactly right for the storefront and exactly wrong as a place to store negotiated contracts. Do not fight that. The gap is a feature, not a limitation to plug.

Architecture: ERP as Pricing Engine

Do not replicate pricing logic in WooCommerce. Use your ERP as the pricing engine and let WooCommerce ask it a question at the moment of truth:

Customer logs into WooCommerce

→ Frontend requests prices for visible products

→ Middleware queries ERP with customer ID + product SKUs

→ ERP returns customer-specific prices

→ Prices displayed in WooCommerce

Customer adds to cart

→ Cart total calculated using ERP prices

→ At checkout, final prices verified against ERP

→ Order created with ERP-validated prices

Implementation: Real-Time Price Lookup

Custom REST Endpoint

// WordPress plugin: ERP Price Endpoint

add_action('rest_api_init', function() {

register_rest_route('b2b/v1', '/prices', [

'methods' => 'POST',

'callback' => 'get_erp_prices',

'permission_callback' => function() {

return is_user_logged_in();

}

]);

});

function get_erp_prices($request) {

$product_ids = $request->get_param('product_ids');

$customer_id = get_user_meta(get_current_user_id(), 'erp_customer_id', true);

if (!$customer_id) {

return new WP_Error('no_erp_account', 'No ERP account linked', ['status' => 400]);

}

// Call ERP API for customer-specific prices

$erp_prices = erp_get_customer_prices($customer_id, $product_ids);

return rest_ensure_response($erp_prices);

}

Middleware Price Service

// Price lookup service

app.post('/api/prices', authenticateCustomer, async (req, res) => {

const { productSkus } = req.body;

const erpCustomerId = req.customer.erpId;

// Cache key: customer + SKU combination

const cacheKey = prices:${erpCustomerId}:${productSkus.sort().join(',')};

const cached = await redis.get(cacheKey);

if (cached) return res.json(JSON.parse(cached));

// Fetch from ERP

const prices = await erp.getCustomerPrices(erpCustomerId, productSkus);

// Transform to WooCommerce format

const result = prices.map(p => ({

sku: p.itemCode,

price: p.customerPrice,

listPrice: p.listPrice,

discount: ((p.listPrice - p.customerPrice) / p.listPrice * 100).toFixed(0),

volumePricing: p.volumeBreaks?.map(vb => ({

minQty: vb.quantity,

price: vb.price

})) || [],

currency: p.currency

}));

// Cache for 15 minutes

await redis.setex(cacheKey, 900, JSON.stringify(result));

res.json(result);

});

Customer Tiers and Role-Based Pricing

Map ERP customer tiers to WooCommerce user roles:

// Sync ERP tiers to WooCommerce roles

function sync_customer_tier($user_id, $erp_tier) {

$tier_role_map = [

'standard' => 'customer',

'silver' => 'b2b_silver',

'gold' => 'b2b_gold',

'platinum' => 'b2b_platinum'

];

$role = $tier_role_map[$erp_tier] ?? 'customer';

$user = new WP_User($user_id);

$user->set_role($role);

}

// Create B2B roles on plugin activation

function create_b2b_roles() {

add_role('b2b_silver', 'B2B Silver', ['read' => true, 'b2b_access' => true]);

add_role('b2b_gold', 'B2B Gold', ['read' => true, 'b2b_access' => true]);

add_role('b2b_platinum', 'B2B Platinum', ['read' => true, 'b2b_access' => true]);

}

Volume Discounts Display

Show volume pricing tables on product pages:

// React component for volume pricing

function VolumePricing({ pricing }) {

if (!pricing.volumePricing?.length) return null;

return (

{pricing.volumePricing.map(vp => (

))}

Quantity Price per Unit Savings
1 - {pricing.volumePricing[0].minQty - 1} ${pricing.price}
{vp.minQty}+ ${vp.price}

{((pricing.price - vp.price) / pricing.price * 100).toFixed(0)}% off

);

}

Quote/RFQ Workflow

For high-value or custom orders, B2B customers expect a quote workflow:

// Request for Quote endpoint

app.post('/api/rfq', authenticateCustomer, async (req, res) => {

const { items, notes } = req.body;

// Create RFQ in ERP

const rfq = await erp.createQuote({

customerId: req.customer.erpId,

items: items.map(i => ({

sku: i.sku,

quantity: i.quantity,

requestedPrice: i.requestedPrice || null

})),

notes,

validUntil: addDays(new Date(), 30)

});

// Notify sales team

await notify.salesTeam({

type: 'new_rfq',

customer: req.customer.name,

totalValue: rfq.estimatedTotal,

rfqId: rfq.id

});

// Create WooCommerce order with "quote" status

const wcOrder = await wooApi.post('orders', {

status: 'quote',

customer_id: req.customer.wcId,

line_items: items.map(i => ({

product_id: i.productId,

quantity: i.quantity,

total: '0' // Price TBD

})),

meta_data: [

{ key: '_rfq_id', value: rfq.id },

{ key: '_rfq_status', value: 'pending' }

]

});

res.json({ rfqId: rfq.id, orderId: wcOrder.data.id });

});

Payment Terms

B2B customers pay on net terms (Net 30, Net 60), not at checkout — expecting a credit card field on a wholesale order is a beginner’s mistake. Handle it through three moving parts:

  • Custom payment gateway: “Purchase Order / Net Terms” that creates the order without payment
  • ERP invoice generation: After order ships, ERP generates the invoice with the agreed terms
  • Credit limit check: Before allowing PO payment, verify the customer’s credit limit in the ERP
// Custom payment gateway: Purchase Order

class WC_Gateway_Purchase_Order extends WC_Payment_Gateway {

public function __construct() {

$this->id = 'purchase_order';

$this->title = 'Purchase Order (Net 30)';

$this->method_title = 'Purchase Order';

$this->has_fields = true;

}

public function payment_fields() {

echo '

Enter your PO number. Payment is due within 30 days of shipment.

';

echo '';

}

public function process_payment($order_id) {

$order = wc_get_order($order_id);

$po_number = sanitize_text_field($_POST['po_number']);

// Verify credit limit with ERP

$customer_erp_id = get_user_meta($order->get_customer_id(), 'erp_customer_id', true);

$credit = erp_check_credit($customer_erp_id, $order->get_total());

if (!$credit['approved']) {

wc_add_notice('Credit limit exceeded. Please contact your account manager.', 'error');

return ['result' => 'failure'];

}

$order->update_meta_data('_po_number', $po_number);

$order->update_status('processing', 'PO received: ' . $po_number);

$order->save();

return ['result' => 'success', 'redirect' => $this->get_return_url($order)];

}

}

Catalog Visibility

B2B stores often need to hide prices or products from non-authenticated visitors:

// Hide prices for non-B2B users

add_filter('woocommerce_get_price_html', function($price, $product) {

if (!current_user_can('b2b_access')) {

return 'Login for pricing';

}

return $price;

}, 10, 2);

// Hide entire products from non-B2B users

add_action('pre_get_posts', function($query) {

if (!is_admin() && $query->is_main_query() && !current_user_can('b2b_access')) {

$query->set('meta_query', [[

'key' => '_b2b_only',

'compare' => 'NOT EXISTS'

]]);

}

});

Where I would spend my caution: the cache window

Notice the 15-minute Redis cache in the middleware above. That number is not decoration — it is the single most consequential decision in this whole design, and it is the one I would make deliberately rather than by copying a tutorial. A price cache is a bet that the customer’s price will not change in the next fifteen minutes. For a stable tier price, that bet is safe and the latency win is worth it. For a contract price with an expiry date, or a volume break that flips at a quantity threshold, a stale cache means the customer sees one number and the ERP charges another. In B2B, that is not a rounding error. That is a disputed invoice and a phone call to an account manager.

So the rule I hold to is simple: display prices can be cached; the order price cannot. Cache aggressively for the catalogue and the cart preview, because those are estimates and the customer knows it. But at checkout, re-fetch from the ERP with no cache in the path and write the ERP-validated number onto the order. The flow above already does this — “at checkout, final prices verified against ERP” — and that line is doing more work than it looks. It is the difference between a fast storefront and a fast storefront that occasionally lies about money. Keep the source of truth the source of truth, and only ever cache a copy you are willing to throw away.

One more caveat worth stating plainly: build the graceful-degradation path before you build the happy path. When the ERP is slow or down — and it will be, during their month-end close, which is also when your biggest buyers are ordering — decide in advance whether the storefront shows the last known price with a warning, falls back to list price, or blocks checkout entirely. Do not let that decision be made for you by an unhandled timeout at 2am.

Conclusion

WooCommerce B2B with ERP integration is about letting each system do what it’s best at. WooCommerce handles the shopping experience — product display, cart, user interface. The ERP handles the business logic — pricing, credit limits, payment terms, volume discounts. The middleware bridges the gap, translating ERP data into WooCommerce-friendly formats in real time. Don’t try to replicate ERP pricing logic in WooCommerce plugins. Query the ERP, cache the results, and let the source of truth remain the source of truth.

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Search Window