Multi-Currency
Serve international customers with region-specific currencies and pricing
This recipe covers setting up multi-currency e-commerce with Hanzo Commerce -- configuring regions, creating currency-specific price lists, handling tax-inclusive pricing, and adding a storefront currency selector.
What You Will Build
- Regions configured with local currencies
- Price lists with per-region pricing
- Tax-inclusive pricing for applicable regions
- Storefront currency selector
- Automatic currency conversion fallback
Configure Regions
Regions define the geographic areas you serve. Each region has a currency, tax settings, and optional payment providers.
import { Commerce } from '@hanzo/commerce'
const commerce = new Commerce({
apiKey: process.env.HANZO_API_KEY!,
environment: 'production',
})
// United States -- prices in USD, tax-exclusive
const us = await commerce.regions.create({
name: 'United States',
currency: 'USD',
taxInclusive: false,
countries: ['US'],
taxProvider: 'auto', // automatic tax calculation
})
// European Union -- prices in EUR, tax-inclusive (VAT)
const eu = await commerce.regions.create({
name: 'European Union',
currency: 'EUR',
taxInclusive: true,
countries: [
'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'PT',
'IE', 'FI', 'SE', 'DK', 'PL', 'CZ', 'RO', 'HU',
],
taxRate: 20, // default VAT rate in percentage
})
// United Kingdom -- prices in GBP, tax-inclusive (VAT)
const uk = await commerce.regions.create({
name: 'United Kingdom',
currency: 'GBP',
taxInclusive: true,
countries: ['GB'],
taxRate: 20,
})
// Japan -- prices in JPY (no minor units)
const jp = await commerce.regions.create({
name: 'Japan',
currency: 'JPY',
taxInclusive: true,
countries: ['JP'],
taxRate: 10,
})Create Price Lists
Price lists let you set explicit prices per region rather than relying on conversion rates. This gives you full control over local pricing strategy.
const product = await commerce.products.retrieve('prod_widget')
// US pricing (base)
await commerce.priceLists.create({
name: 'US Pricing',
regionId: us.id,
prices: [
{ productId: product.id, amount: 2999 }, // $29.99
],
})
// EU pricing (tax-inclusive, rounded to .00)
await commerce.priceLists.create({
name: 'EU Pricing',
regionId: eu.id,
prices: [
{ productId: product.id, amount: 2900 }, // 29.00 EUR (incl. VAT)
],
})
// UK pricing
await commerce.priceLists.create({
name: 'UK Pricing',
regionId: uk.id,
prices: [
{ productId: product.id, amount: 2499 }, // 24.99 GBP (incl. VAT)
],
})
// Japan pricing (JPY has no minor units, so 3200 = 3200 JPY)
await commerce.priceLists.create({
name: 'Japan Pricing',
regionId: jp.id,
prices: [
{ productId: product.id, amount: 3200 }, // 3,200 JPY (incl. tax)
],
})Automatic Currency Conversion
For regions without explicit price lists, Commerce can fall back to automatic conversion:
await commerce.settings.update({
currencyConversion: {
enabled: true,
baseCurrency: 'USD',
provider: 'ecb', // European Central Bank rates
refreshInterval: '6h',
roundingMode: 'up', // round converted prices up to nearest unit
},
})Conversion providers:
| Provider | Source | Update Frequency |
|---|---|---|
ecb | European Central Bank | Daily |
openexchangerates | Open Exchange Rates API | Hourly |
fixed | Manual rates you set | On demand |
Tax-Inclusive Pricing
In tax-inclusive regions (EU, UK, JP), the price displayed to the customer already includes tax. Commerce handles the math:
Product price (EU): 29.00 EUR (tax-inclusive)
Tax rate: 20% VAT
Net price: 24.17 EUR
Tax amount: 4.83 EUR
Customer pays: 29.00 EURCommerce breaks this down automatically on invoices and in the order object:
{
"subtotal": 2417,
"taxTotal": 483,
"total": 2900,
"taxInclusive": true
}Storefront Region Detection
Detect the customer's region automatically and allow manual override.
import { commerce } from './commerce'
export async function detectRegion(request: Request): Promise<string> {
// Try geo-IP from request headers (e.g., Vercel's x-vercel-ip-country)
const country = request.headers.get('x-vercel-ip-country')
if (country) {
const region = await commerce.regions.getByCountry(country)
if (region) return region.id
}
// Fallback to default region
return process.env.DEFAULT_REGION_ID!
}Currency Selector Component
Let customers override the auto-detected region:
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
const regions = [
{ id: 'reg_us', label: 'USD $', flag: 'US' },
{ id: 'reg_eu', label: 'EUR', flag: 'EU' },
{ id: 'reg_uk', label: 'GBP', flag: 'GB' },
{ id: 'reg_jp', label: 'JPY', flag: 'JP' },
]
export function CurrencySelector({ currentRegionId }: { currentRegionId: string }) {
const router = useRouter()
const [selected, setSelected] = useState(currentRegionId)
function handleChange(regionId: string) {
setSelected(regionId)
// Store in cookie for server-side access
document.cookie = `region=${regionId}; path=/; max-age=31536000`
router.refresh()
}
return (
<select
value={selected}
onChange={(e) => handleChange(e.target.value)}
className="border rounded px-2 py-1 text-sm"
>
{regions.map((r) => (
<option key={r.id} value={r.id}>
{r.flag} {r.label}
</option>
))}
</select>
)
}Fetching Region-Aware Prices
Pass the region ID when fetching products to get localized prices:
import { commerce } from '@/lib/commerce'
import { cookies } from 'next/headers'
export default async function ProductsPage() {
const regionId = cookies().get('region')?.value ?? process.env.DEFAULT_REGION_ID!
const { products } = await commerce.products.list({
regionId,
limit: 20,
})
// products[].price is now in the region's currency
// products[].currency reflects the region's currency code
}Cart and Checkout
Carts are region-scoped. Set the region when creating the cart:
const cart = await commerce.carts.create({
regionId,
items: [{ productId: 'prod_widget', quantity: 1 }],
})
// cart.total, cart.taxTotal, cart.currency all reflect the regionTesting
- Create at least two regions (USD and EUR)
- Create a product with price lists for both regions
- Fetch the product with each
regionId-- verify prices differ - Create a cart in each region -- verify totals use the correct currency
- Complete checkout in each region -- verify payment is charged in the correct currency
Next Steps
- Review the Region module for advanced region configuration
- Set up the Tax module for automatic tax calculation
- Add B2B pricing with customer-group-specific price lists
How is this guide?
Last updated on