DEV Community

Cover image for Multi-Currency E-commerce: Stop Losing Sales
APIVerve
APIVerve

Posted on • Originally published at blog.apiverve.com

Multi-Currency E-commerce: Stop Losing Sales

A friend runs an online store selling specialty coffee equipment. Ships worldwide. Most of his traffic comes from outside the US, but for three years, his site only showed USD prices.

One day he added a currency switcher. Same products, same prices, just displayed in the visitor's local currency.

Conversions from international visitors went up 34%.

That's it. That's the whole story. He didn't lower prices. Didn't change shipping. Didn't run any promotions. Just stopped making European customers do mental math to figure out how much a grinder costs in euros.

Why Local Currency Matters

Here's what happens in a customer's brain when they see "$249.99":

If they think in USD: "Two fifty, reasonable for a grinder."

If they think in EUR: "Two fifty... divide by... what's the rate... maybe €230? Or €210? Let me Google it... actually, maybe I'll buy this later."

That "maybe later" is a lost sale. Every second of friction is a customer who might not come back.

Studies consistently show:

  • 33% of shoppers abandon carts when prices aren't in their local currency
  • 92% prefer to shop on sites that price in their currency
  • Conversion rates increase 20-40% when local currency is shown

You're not just being helpful. You're removing a barrier that costs you money.

The Basic Implementation

Here's the simplest version: detect the visitor's location, convert your prices, display in their currency.

async function getVisitorCurrency(ip) {
  const res = await fetch(
    `https://api.apiverve.com/v1/iplookup?ip=${ip}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  // Map country to currency (simplified - you'd want a full mapping)
  const currencyMap = {
    'United States': 'USD',
    'United Kingdom': 'GBP',
    'Germany': 'EUR',
    'France': 'EUR',
    'Japan': 'JPY',
    'Canada': 'CAD',
    'Australia': 'AUD'
    // ... etc
  };

  return currencyMap[data.country] || 'USD';
}

async function convertPrice(amount, fromCurrency, toCurrency) {
  if (fromCurrency === toCurrency) return amount;

  const res = await fetch(
    `https://api.apiverve.com/v1/currencyconverter?value=${amount}&from=${fromCurrency}&to=${toCurrency}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return data.convertedValue;
}

// Usage
const visitorCurrency = await getVisitorCurrency(req.ip);
const localPrice = await convertPrice(249.99, 'USD', visitorCurrency);

console.log(`$249.99 = ${formatCurrency(localPrice, visitorCurrency)}`);
// $249.99 = €228.42 (for EUR visitor)
Enter fullscreen mode Exit fullscreen mode

Don't Convert Every Request

The code above works, but calling the API on every page load is wasteful. Exchange rates don't change every second. Cache them.

const rateCache = new Map();

async function getExchangeRate(from, to) {
  const key = `${from}:${to}`;
  const cached = rateCache.get(key);

  // Rates cached for 1 hour
  if (cached && Date.now() - cached.timestamp < 3600000) {
    return cached.rate;
  }

  const res = await fetch(
    `https://api.apiverve.com/v1/exchangerate?currency1=${from}&currency2=${to}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  rateCache.set(key, {
    rate: data.rate,
    timestamp: Date.now()
  });

  return data.rate;
}

function convertPrice(amount, rate) {
  return amount * rate;
}

// Fetch rate once, use for all products
const rate = await getExchangeRate('USD', 'EUR');
const prices = products.map(p => ({
  ...p,
  localPrice: convertPrice(p.price, rate)
}));
Enter fullscreen mode Exit fullscreen mode

Now you're making one API call per currency pair per hour, instead of thousands.

The Currency Switcher

Automatic detection is great, but some customers prefer manual control. Maybe they're traveling. Maybe they're buying a gift for someone abroad. Give them a dropdown.

// React component
function CurrencySwitcher({ currentCurrency, onCurrencyChange }) {
  const currencies = [
    { code: 'USD', symbol: '$', name: 'US Dollar' },
    { code: 'EUR', symbol: '', name: 'Euro' },
    { code: 'GBP', symbol: '£', name: 'British Pound' },
    { code: 'CAD', symbol: 'CA$', name: 'Canadian Dollar' },
    { code: 'AUD', symbol: 'A$', name: 'Australian Dollar' },
    { code: 'JPY', symbol: '¥', name: 'Japanese Yen' }
  ];

  return (
    <select
      value={currentCurrency}
      onChange={e => onCurrencyChange(e.target.value)}
    >
      {currencies.map(c => (
        <option key={c.code} value={c.code}>
          {c.symbol} {c.code} - {c.name}
        </option>
      ))}
    </select>
  );
}
Enter fullscreen mode Exit fullscreen mode

Store the selection in a cookie or localStorage so it persists across visits:

function setCurrencyPreference(currency) {
  localStorage.setItem('preferredCurrency', currency);
  document.cookie = `currency=${currency}; path=/; max-age=31536000`; // 1 year
}

function getCurrencyPreference() {
  return localStorage.getItem('preferredCurrency') ||
         document.cookie.match(/currency=([A-Z]{3})/)?.[1] ||
         null;
}
Enter fullscreen mode Exit fullscreen mode

The logic becomes: Use stored preference → fall back to auto-detect → default to USD.

Pricing Psychology: Round Numbers vs Precision

Here's where it gets interesting. When you convert $249.99 USD to EUR, you might get €228.47. That looks... weird. It screams "this was converted from another currency."

You have two options:

Option 1: Show exact conversion (transparent)

formatCurrency(228.47, 'EUR'); // €228.47
Enter fullscreen mode Exit fullscreen mode

Pros: Honest, matches what they'll actually pay, no surprises at checkout.

Option 2: Round to psychological price points

function roundToPsychPrice(price) {
  if (price < 10) return Math.ceil(price) - 0.01;      // €8.99
  if (price < 100) return Math.ceil(price / 5) * 5 - 0.01;  // €44.99
  return Math.ceil(price / 10) * 10 - 0.01;            // €229.99
}

roundToPsychPrice(228.47); // €229.99
Enter fullscreen mode Exit fullscreen mode

Pros: Looks more "native," feels like local pricing, standard retail practice.

The second option means you might charge slightly more (or less) than the exact conversion. Most retailers do this—they set different prices for different markets rather than converting dynamically.

My recommendation: Be transparent. Show the exact conversion, note that it's converted from USD, and make it clear the charge will be in your base currency. Customers appreciate honesty.

The Checkout Problem

Here's a trap: you show EUR prices, but you charge in USD because that's what your payment processor is set up for.

Customer sees: €228.47
Customer gets charged: $249.99 (converted by their bank to ~€231.20)

They feel cheated. They might dispute the charge. At minimum, they won't buy from you again.

Solutions:

  1. Show both currencies at checkout
   Total: €228.47
   (approximately $249.99 USD - you will be charged in USD)
Enter fullscreen mode Exit fullscreen mode
  1. Use a payment processor that supports multi-currency
    Stripe, PayPal, and others let you charge in the customer's currency. You receive your preferred currency; they handle the conversion.

  2. Set fixed prices in each currency
    Instead of converting dynamically, set specific prices for each market:

    • US: $249.99
    • EU: €229.99
    • UK: £199.99

Update periodically when exchange rates shift significantly.

Option 3 is what big retailers do. It's more work but gives you control over pricing in each market.

Complete Implementation

Here's a full multi-currency service:

class CurrencyService {
  constructor(baseCurrency = 'USD') {
    this.baseCurrency = baseCurrency;
    this.rates = new Map();
    this.lastUpdate = 0;
  }

  async refreshRates() {
    // Fetch rates for common currencies
    const currencies = ['EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'CHF', 'CNY', 'INR'];

    await Promise.all(currencies.map(async (currency) => {
      try {
        const res = await fetch(
          `https://api.apiverve.com/v1/exchangerate?currency1=${this.baseCurrency}&currency2=${currency}`,
          { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
        );
        const { data } = await res.json();
        this.rates.set(currency, data.rate);
      } catch (err) {
        console.error(`Failed to fetch ${currency} rate:`, err);
      }
    }));

    this.rates.set(this.baseCurrency, 1); // Base currency rate is 1
    this.lastUpdate = Date.now();
  }

  async getRate(targetCurrency) {
    // Refresh if rates are older than 1 hour
    if (Date.now() - this.lastUpdate > 3600000) {
      await this.refreshRates();
    }

    return this.rates.get(targetCurrency) || 1;
  }

  async convertPrice(amount, targetCurrency) {
    const rate = await this.getRate(targetCurrency);
    return amount * rate;
  }

  async convertPrices(products, targetCurrency) {
    const rate = await this.getRate(targetCurrency);

    return products.map(product => ({
      ...product,
      displayPrice: product.price * rate,
      displayCurrency: targetCurrency,
      originalPrice: product.price,
      originalCurrency: this.baseCurrency
    }));
  }
}

// Initialize once
const currencyService = new CurrencyService('USD');

// Pre-fetch rates on server start
currencyService.refreshRates();

// Schedule hourly refresh
setInterval(() => currencyService.refreshRates(), 3600000);

// Use in route
app.get('/products', async (req, res) => {
  const currency = req.cookies.currency ||
                   await detectCurrencyFromIP(req.ip) ||
                   'USD';

  const products = await getProducts();
  const localizedProducts = await currencyService.convertPrices(products, currency);

  res.json({
    products: localizedProducts,
    currency,
    exchangeRate: await currencyService.getRate(currency),
    rateUpdated: new Date(currencyService.lastUpdate).toISOString()
  });
});
Enter fullscreen mode Exit fullscreen mode

Formatting Currencies Correctly

Different currencies have different conventions:

function formatCurrency(amount, currencyCode) {
  return new Intl.NumberFormat(getCurrencyLocale(currencyCode), {
    style: 'currency',
    currency: currencyCode,
    minimumFractionDigits: currencyCode === 'JPY' ? 0 : 2,
    maximumFractionDigits: currencyCode === 'JPY' ? 0 : 2
  }).format(amount);
}

function getCurrencyLocale(currencyCode) {
  const locales = {
    'USD': 'en-US',
    'EUR': 'de-DE',
    'GBP': 'en-GB',
    'JPY': 'ja-JP',
    'CAD': 'en-CA',
    'AUD': 'en-AU',
    'CHF': 'de-CH',
    'CNY': 'zh-CN'
  };
  return locales[currencyCode] || 'en-US';
}

// Results:
formatCurrency(1234.56, 'USD');  // $1,234.56
formatCurrency(1234.56, 'EUR');  // 1.234,56 €
formatCurrency(1234.56, 'GBP');  // £1,234.56
formatCurrency(1234.56, 'JPY');  // ¥1,235 (no decimals)
Enter fullscreen mode Exit fullscreen mode

Note: EUR uses comma for decimals and period for thousands (in Germany). Japanese Yen doesn't use decimals. Get this wrong and you look like an amateur.

Common Mistakes

Showing converted prices but charging in base currency without warning. This is the fastest way to get chargebacks and angry customers. Always be clear about what currency they'll actually be charged in.

Not handling currency in the cart. If someone adds an item at €228 and the rate changes before checkout, what happens? Pick a strategy: lock the rate at add-to-cart, or clearly show that prices may fluctuate.

Forgetting about taxes. VAT in Europe, GST in Australia, sales tax in US states. Currency conversion is only part of international pricing.

Updating rates too frequently. Hourly is fine. Daily is fine. Every request is wasteful and can cause prices to flicker confusingly.

Not rounding sensibly. €228.47192 is not a price. Neither is €228.471923847. Round to two decimals (or zero for JPY).


Multi-currency support isn't just a nice-to-have for international stores. It's table stakes. Every competitor who shows local prices is stealing your customers.

The good news: it's not that hard. Detect location, fetch exchange rates, cache them, display converted prices. A weekend project that pays for itself in the first month.

The Currency Converter and Exchange Rate APIs handle the conversion. The IP Lookup API tells you where your visitors are. Combine them and you've got localized pricing.

Get your API key and stop losing international sales.


Originally published at APIVerve Blog

Top comments (0)