B2B Commerce
Enterprise commerce with customer groups, bulk ordering, and purchase orders
This recipe covers setting up B2B (business-to-business) commerce with Hanzo Commerce -- customer groups for tiered pricing, bulk ordering, purchase orders with net terms, company accounts, and quote requests.
What You Will Build
- Customer groups with tier-based pricing
- Bulk ordering with quantity discounts
- Purchase order and net-terms payment flow
- Company accounts with multiple buyers
- Quote request and approval workflow
Customer Groups
Customer groups let you segment business customers by tier and assign group-specific pricing.
import { Commerce } from '@hanzo/commerce'
const commerce = new Commerce({
apiKey: process.env.HANZO_API_KEY!,
environment: 'production',
})
const wholesale = await commerce.customerGroups.create({
name: 'Wholesale',
metadata: { tier: 'wholesale', discount: 30 },
})
const distributor = await commerce.customerGroups.create({
name: 'Distributor',
metadata: { tier: 'distributor', discount: 45 },
})
const enterprise = await commerce.customerGroups.create({
name: 'Enterprise',
metadata: { tier: 'enterprise', discount: 'custom' },
})Assign a customer to a group:
await commerce.customers.update(customerId, {
groupId: wholesale.id,
})Tiered Price Lists
Create price lists scoped to customer groups. These override the default retail price.
// Retail price: $100.00
const product = await commerce.products.create({
name: 'Industrial Widget',
slug: 'industrial-widget',
price: 10000,
currency: 'USD',
})
// Wholesale price: $70.00 (30% off)
await commerce.priceLists.create({
name: 'Wholesale Pricing',
customerGroupId: wholesale.id,
prices: [
{ productId: product.id, amount: 7000 },
],
})
// Distributor price: $55.00 (45% off)
await commerce.priceLists.create({
name: 'Distributor Pricing',
customerGroupId: distributor.id,
prices: [
{ productId: product.id, amount: 5500 },
],
})When a customer in the Wholesale group views products, they see $70.00 automatically.
Quantity Discounts
Add volume-based pricing tiers to encourage larger orders:
await commerce.priceLists.create({
name: 'Wholesale Volume Pricing',
customerGroupId: wholesale.id,
prices: [
{
productId: product.id,
tiers: [
{ minQuantity: 1, amount: 7000 }, // 1-49: $70.00 each
{ minQuantity: 50, amount: 6000 }, // 50-99: $60.00 each
{ minQuantity: 100, amount: 5000 }, // 100-499: $50.00 each
{ minQuantity: 500, amount: 4000 }, // 500+: $40.00 each
],
},
],
})The cart automatically applies the correct tier based on quantity:
const cart = await commerce.carts.create({
customerId: wholesaleCustomer.id,
items: [
{ productId: product.id, quantity: 200 },
// Price: 200 x $50.00 = $10,000.00
],
})Company Accounts
B2B customers often have multiple buyers under one company. Company accounts group individual users under a shared billing entity.
const company = await commerce.companies.create({
name: 'Acme Corp',
email: '[email protected]',
groupId: wholesale.id,
billingAddress: {
line1: '100 Industrial Blvd',
city: 'Chicago',
state: 'IL',
postalCode: '60601',
country: 'US',
},
settings: {
paymentMethods: ['credit_card', 'purchase_order'],
purchaseOrderLimit: 5000000, // $50,000.00
netTerms: 30, // Net 30 days
},
})
// Add buyers to the company
await commerce.companies.addMember(company.id, {
customerId: buyerOne.id,
role: 'admin', // can manage company settings
})
await commerce.companies.addMember(company.id, {
customerId: buyerTwo.id,
role: 'buyer', // can place orders
})Company Roles
| Role | Permissions |
|---|---|
admin | Manage members, update billing, view all orders, approve POs |
buyer | Place orders, view own orders, submit POs for approval |
viewer | View orders and company info only |
Purchase Orders
Enterprise customers often pay by purchase order (PO) with net terms instead of credit card.
Submitting a Purchase Order
const order = await commerce.orders.create({
companyId: company.id,
customerId: buyerOne.id,
items: [
{ productId: product.id, quantity: 500 },
],
paymentMethod: 'purchase_order',
purchaseOrder: {
number: 'PO-2026-0042',
terms: 'net_30',
},
})Purchase Order Flow
submitted --> pending_approval --> approved --> invoiced --> paid
\-> rejected- Buyer submits an order with a PO number
- Company admin receives an approval notification
- Admin approves or rejects the PO
- On approval, Commerce generates an invoice with net-30 terms
- Invoice is sent to the company billing email
- Payment is recorded when received
Approving a PO
// Company admin approves
await commerce.orders.approvePurchaseOrder(order.id, {
approvedBy: adminCustomerId,
})
// Or reject with a reason
await commerce.orders.rejectPurchaseOrder(order.id, {
rejectedBy: adminCustomerId,
reason: 'Exceeds quarterly budget',
})Net Terms and Invoicing
Configure net terms per company:
await commerce.companies.update(company.id, {
settings: {
netTerms: 30, // Net 30 (default)
// netTerms: 60, // Net 60
// netTerms: 0, // Due on receipt
},
})Commerce automatically:
- Generates invoices on order approval
- Sends invoice emails to the company billing contact
- Tracks payment due dates
- Sends reminders at 7 days and 1 day before due
- Marks invoices as
overdueafter the net-terms period
// List outstanding invoices for a company
const invoices = await commerce.invoices.list({
companyId: company.id,
status: 'unpaid',
})Quote Requests
For enterprise deals or custom pricing, support a quote-request workflow:
// Buyer requests a quote
const quote = await commerce.quotes.create({
companyId: company.id,
customerId: buyerOne.id,
items: [
{ productId: product.id, quantity: 10000, note: 'Annual supply contract' },
],
message: 'Requesting volume pricing for annual contract.',
})Quote Flow
requested --> draft --> sent --> accepted --> converted_to_order
\-> declined
\-> expiredAdmin responds with a quote:
await commerce.quotes.send(quote.id, {
items: [
{ productId: product.id, quantity: 10000, amount: 3500 }, // $35.00 each
],
validUntil: '2026-03-16T00:00:00Z',
message: 'Special annual contract pricing. Valid for 30 days.',
})Customer accepts:
await commerce.quotes.accept(quote.id)
// Automatically creates an order with the quoted pricesB2B Storefront Considerations
When building a B2B storefront, account for these differences from B2C:
import { commerce } from '@/lib/commerce'
import { auth } from '@/lib/auth'
export default async function ProductsPage() {
const session = await auth()
const customer = session?.customerId
? await commerce.customers.retrieve(session.customerId)
: null
// Fetch products with customer-group pricing
const { products } = await commerce.products.list({
customerId: customer?.id, // applies group pricing
limit: 50,
})
// Show quantity input instead of simple "Add to Cart"
// Show "Request Quote" button for large quantities
// Show PO payment option at checkout if company allows it
}Testing
- Create customer groups (Wholesale, Distributor)
- Create tiered price lists for each group
- Assign a customer to a group and verify they see group-specific prices
- Test quantity discounts by adding items with different quantities
- Create a company account and add members
- Submit a purchase order and test the approval flow
- Verify invoice generation and net-terms due dates
- Submit a quote request and test the accept/reject flow
Next Steps
- Add multi-currency support for international B2B customers
- Set up analytics to track B2B metrics
- Review the Customer module for advanced group configuration
How is this guide?
Last updated on