Marketplace
Build a multi-vendor marketplace with split payments and vendor management
This recipe walks through building a multi-vendor marketplace on Hanzo Commerce -- vendor onboarding, product management per vendor, split payments, commission tracking, and vendor dashboards.
What You Will Build
- Multi-tenant vendor system using namespaces
- Vendor onboarding and approval flow
- Per-vendor product catalogs
- Split payments at checkout
- Commission tracking and payouts
- Vendor dashboard for order management
Architecture Overview
Customer --> Storefront --> Commerce API
|
+---------------+---------------+
| | |
Vendor A Vendor B Platform
(namespace) (namespace) (owner)
| | |
Products Products Commission
Orders Orders Payouts
Fulfillment Fulfillment AnalyticsHanzo Commerce uses namespaces for multi-tenancy. Each vendor operates within an isolated namespace with their own products, orders, and fulfillment, while the platform owner controls global settings, commissions, and payouts.
Enable Marketplace Mode
Configure your Commerce instance for marketplace operation:
import { Commerce } from '@hanzo/commerce'
const commerce = new Commerce({
apiKey: process.env.HANZO_API_KEY!,
environment: 'production',
})
await commerce.settings.update({
marketplace: {
enabled: true,
commissionRate: 15, // 15% platform commission
payoutSchedule: 'weekly', // weekly vendor payouts
payoutMinimum: 5000, // $50.00 minimum payout
vendorApproval: 'manual', // require admin approval
},
})Vendor Onboarding
Registration Endpoint
Create an API route for vendor applications:
import { commerce } from '@/lib/commerce'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const body = await req.json()
const vendor = await commerce.vendors.create({
name: body.businessName,
email: body.email,
description: body.description,
metadata: {
taxId: body.taxId,
address: body.address,
},
})
// Vendor is in "pending" status until admin approves
return NextResponse.json({ vendorId: vendor.id, status: vendor.status })
}Admin Approval
Approve vendors from the Admin dashboard or via the API:
// Approve a vendor -- creates their namespace and credentials
const vendor = await commerce.vendors.approve(vendorId)
// vendor.namespace -- unique namespace identifier
// vendor.apiKey -- vendor-scoped API key
// vendor.dashboardUrl -- vendor dashboard linkVendor Status Flow
applied --> pending_review --> approved --> active
\-> rejectedVendor Product Management
Vendors manage products within their namespace. Use the vendor-scoped SDK:
import { Commerce } from '@hanzo/commerce'
// Vendor uses their own API key
const vendorCommerce = new Commerce({
apiKey: vendor.apiKey,
namespace: vendor.namespace,
})
const product = await vendorCommerce.products.create({
name: 'Handmade Pottery Bowl',
slug: 'handmade-pottery-bowl',
price: 4500, // $45.00
currency: 'USD',
images: [{ url: 'https://...' }],
})Products created in a vendor namespace:
- Are owned by the vendor
- Appear in the global marketplace catalog
- Route orders to the vendor for fulfillment
- Apply the platform commission rate
Split Payments
When a customer buys from multiple vendors in one cart, Commerce splits the payment automatically.
// Customer adds items from different vendors
const cart = await commerce.carts.create({
items: [
{ productId: 'prod_vendor_a_item', quantity: 1 },
{ productId: 'prod_vendor_b_item', quantity: 2 },
],
})
// Checkout creates a single charge, Commerce handles splits
const session = await commerce.checkout.create({
cartId: cart.id,
successUrl: 'https://marketplace.example.com/order/confirmation',
cancelUrl: 'https://marketplace.example.com/cart',
})The resulting order contains split information:
{
"id": "order_abc123",
"total": 13500,
"splits": [
{
"vendorId": "vendor_a",
"subtotal": 4500,
"commission": 675,
"vendorPayout": 3825
},
{
"vendorId": "vendor_b",
"subtotal": 9000,
"commission": 1350,
"vendorPayout": 7650
}
]
}Commission Configuration
Set commission rates globally or per vendor:
// Global default
await commerce.settings.update({
marketplace: { commissionRate: 15 },
})
// Per-vendor override (e.g., lower rate for top sellers)
await commerce.vendors.update(vendorId, {
commissionRate: 10, // 10% for this vendor
})
// Per-category override
await commerce.categories.update(categoryId, {
commissionRate: 20, // 20% for this category
})Commission priority: product > category > vendor > global.
Vendor Payouts
Commerce tracks vendor balances and handles payouts:
// Get vendor balance
const balance = await commerce.vendors.getBalance(vendorId)
// { available: 125000, pending: 34500, currency: 'USD' }
// Trigger a manual payout
const payout = await commerce.payouts.create({
vendorId,
amount: balance.available,
method: 'bank_transfer', // or 'stripe_connect'
})Payout Methods
| Method | Description |
|---|---|
stripe_connect | Direct to vendor's Stripe account |
bank_transfer | ACH/wire to vendor's bank account |
paypal | PayPal payout |
manual | Platform admin handles manually |
Vendor Dashboard
Vendors get a scoped view of their business:
import { vendorCommerce } from '@/lib/vendor-commerce'
export default async function VendorDashboard() {
const [orders, products, balance] = await Promise.all([
vendorCommerce.orders.list({ limit: 10, sort: '-createdAt' }),
vendorCommerce.products.list({ limit: 100 }),
vendorCommerce.balance.get(),
])
return (
<main className="max-w-6xl mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-6">Vendor Dashboard</h1>
<div className="grid grid-cols-3 gap-4 mb-8">
<StatCard title="Products" value={products.total} />
<StatCard title="Orders" value={orders.total} />
<StatCard
title="Balance"
value={`$${(balance.available / 100).toFixed(2)}`}
/>
</div>
<h2 className="text-xl font-semibold mb-4">Recent Orders</h2>
<OrdersTable orders={orders.data} />
</main>
)
}Vendor Fulfillment
Each vendor fulfills their portion of an order independently:
// Vendor marks their items as shipped
await vendorCommerce.fulfillments.create({
orderId: order.id,
items: [{ itemId: 'item_123', quantity: 1 }],
trackingNumber: '1Z999AA10123456784',
carrier: 'ups',
})The platform order status updates to partially_fulfilled until all vendors have shipped, then moves to fulfilled.
Testing
- Enable marketplace mode and create two vendor accounts
- Approve both vendors and verify namespace creation
- Create products under each vendor
- Add products from both vendors to a single cart
- Complete checkout -- verify the order splits and commission amounts
- Check vendor balances reflect the correct payout amounts
- Test vendor fulfillment for each split
Next Steps
- Add multi-currency support for international vendors
- Review the Payment module for Stripe Connect setup
- Set up analytics for marketplace-wide reporting
How is this guide?
Last updated on