/stripe-guides

How to handle multiple currencies in Stripe?

Learn how to set up, manage, and display multiple currencies in Stripe, including payments, subscriptions, reporting, and user currency switching.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to handle multiple currencies in Stripe?

How to Handle Multiple Currencies in Stripe: A Comprehensive Guide

 

Step 1: Set Up Your Stripe Account for Multi-Currency Support

 

Before implementing multiple currencies in your application, ensure your Stripe account is properly configured:

// First, log in to your Stripe Dashboard
// Navigate to Settings > Account settings > Currency settings
// Enable the currencies you want to support

Note that Stripe supports displaying prices in multiple currencies but typically settles in your account's default currency, potentially incurring conversion fees.

 

Step 2: Initialize Stripe with API Keys

 

Set up your Stripe client with your API keys:

const stripe = require('stripe')('sk_test_your_secret_key');

// For frontend (using Stripe.js)


 

Step 3: Create Products with Multiple Currency Prices

 

When creating products, you can specify prices in different currencies:

// Create a product
const product = await stripe.products.create({
  name: 'Premium Subscription',
  description: 'Monthly premium subscription',
});

// Create prices for this product in different currencies
const usdPrice = await stripe.prices.create({
  product: product.id,
  unit\_amount: 1500, // $15.00
  currency: 'usd',
  recurring: {
    interval: 'month',
  },
});

const eurPrice = await stripe.prices.create({
  product: product.id,
  unit\_amount: 1300, // €13.00
  currency: 'eur',
  recurring: {
    interval: 'month',
  },
});

const gbpPrice = await stripe.prices.create({
  product: product.id,
  unit\_amount: 1100, // £11.00
  currency: 'gbp',
  recurring: {
    interval: 'month',
  },
});

 

Step 4: Store Currency-Price Mappings in Your Database

 

Create a structure to store price IDs for each currency:

// Example database schema (pseudocode)
const ProductSchema = {
  stripeProductId: String,
  name: String,
  description: String,
  prices: {
    usd: { priceId: String, amount: Number },
    eur: { priceId: String, amount: Number },
    gbp: { priceId: String, amount: Number },
    // other currencies as needed
  }
};

// Store the product with its price IDs
const productRecord = {
  stripeProductId: product.id,
  name: 'Premium Subscription',
  description: 'Monthly premium subscription',
  prices: {
    usd: { priceId: usdPrice.id, amount: 1500 },
    eur: { priceId: eurPrice.id, amount: 1300 },
    gbp: { priceId: gbpPrice.id, amount: 1100 }
  }
};

 

Step 5: Determine the User's Preferred Currency

 

Implement logic to determine which currency to use:

function getUserCurrency(req) {
  // Option 1: User preference (stored in profile)
  if (req.user && req.user.preferredCurrency) {
    return req.user.preferredCurrency;
  }
  
  // Option 2: Geographic detection
  const countryToDefaultCurrency = {
    'US': 'usd',
    'CA': 'cad',
    'GB': 'gbp',
    'DE': 'eur',
    'FR': 'eur',
    // Add more country-currency mappings as needed
  };
  
  if (req.headers['cf-ipcountry']) { // If using Cloudflare
    const country = req.headers['cf-ipcountry'];
    if (countryToDefaultCurrency[country]) {
      return countryToDefaultCurrency[country];
    }
  }
  
  // Option 3: Browser locale detection (frontend JavaScript)
  /\* 
  const locale = navigator.language || navigator.userLanguage;
  const currency = getCurrencyFromLocale(locale);
  \*/
  
  // Fallback to default currency
  return 'usd';
}

 

Step 6: Create Payment Intents with Appropriate Currency

 

When creating a Payment Intent, specify the correct currency:

async function createPayment(productId, currency) {
  // Fetch product from your database
  const product = await YourDatabase.findProduct(productId);
  
  // Check if the product has a price in the requested currency
  if (!product.prices[currency]) {
    throw new Error(`Price not available in ${currency}`);
  }
  
  // Create a payment intent with the appropriate currency
  const paymentIntent = await stripe.paymentIntents.create({
    amount: product.prices[currency].amount,
    currency: currency,
    payment_method_types: ['card'],
    metadata: {
      productId: productId
    }
  });
  
  return paymentIntent;
}

 

Step 7: Display Prices to Users

 

Format prices appropriately for display:

function formatPrice(amount, currency) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency,
    minimumFractionDigits: 2
  }).format(amount / 100); // Stripe amounts are in cents
}

// Example usage
const price = formatPrice(product.prices['usd'].amount, 'usd');
// Output: $15.00

// Frontend example with React
function PriceDisplay({ product, currency }) {
  const price = formatPrice(product.prices[currency].amount, currency);
  return 
{price}
; }

 

Step 8: Create Checkout Sessions with Currency Support

 

For Stripe Checkout, specify the currency:

async function createCheckoutSession(productId, currency) {
  // Fetch product from your database
  const product = await YourDatabase.findProduct(productId);
  
  // Get the appropriate price ID for the currency
  const priceId = product.prices[currency].priceId;
  
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line\_items: [
      {
        price: priceId,
        quantity: 1,
      },
    ],
    mode: 'payment',
    success_url: 'https://your-website.com/success?session_id={CHECKOUT_SESSION_ID}',
    cancel\_url: 'https://your-website.com/cancel',
    locale: currency === 'eur' ? 'fr' : 'en', // Optionally customize checkout locale
  });
  
  return session;
}

 

Step 9: Handle Currency Conversion for Reporting

 

For reporting and analytics, you might need to convert all transactions to a base currency:

async function getMonthlyRevenue(month, year) {
  // Fetch payments for the given month
  const payments = await stripe.paymentIntents.list({
    created: {
      gte: new Date(year, month-1, 1).getTime() / 1000,
      lt: new Date(year, month, 1).getTime() / 1000
    }
  });
  
  // Group by currency
  const revenueByCurrency = {};
  for (const payment of payments.data) {
    if (payment.status === 'succeeded') {
      const currency = payment.currency;
      if (!revenueByCurrency[currency]) {
        revenueByCurrency[currency] = 0;
      }
      revenueByCurrency[currency] += payment.amount;
    }
  }
  
  // Convert to base currency (USD) for reporting
  // Note: You'll need an exchange rate API for accurate conversion
  const baseRevenue = await convertToBaseCurrency(revenueByCurrency, 'usd');
  
  return {
    byCurrency: revenueByCurrency,
    totalInUSD: baseRevenue
  };
}

async function convertToBaseCurrency(revenueByCurrency, baseCurrency) {
  // Example implementation using an exchange rate API
  let totalInBaseCurrency = 0;
  
  for (const currency in revenueByCurrency) {
    if (currency === baseCurrency) {
      totalInBaseCurrency += revenueByCurrency[currency];
    } else {
      // Get exchange rate (you'd implement this with an API)
      const rate = await getExchangeRate(currency, baseCurrency);
      totalInBaseCurrency += revenueByCurrency[currency] \* rate;
    }
  }
  
  return totalInBaseCurrency;
}

 

Step 10: Handle Webhooks for Multi-Currency Payments

 

Configure webhook handlers to process events from payments in different currencies:

app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      'whsec_your_webhook\_secret'
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payment\_intent.succeeded') {
    const paymentIntent = event.data.object;
    const currency = paymentIntent.currency;
    const amountPaid = paymentIntent.amount;
    
    // Process successful payment
    // Consider currency-specific logic if needed
    await processSuccessfulPayment(paymentIntent.id, amountPaid, currency);
  }

  res.status(200).send();
});

async function processSuccessfulPayment(paymentIntentId, amount, currency) {
  // Record the payment in your database
  await YourDatabase.recordPayment({
    stripePaymentId: paymentIntentId,
    amount: amount,
    currency: currency,
    timestamp: new Date(),
    // Additional details as needed
  });
  
  // Handle currency-specific business logic if needed
  if (currency === 'jpy') {
    // Japanese Yen payments might need special processing
    await sendJapaneseReceiptEmail(paymentIntentId);
  }
}

 

Step 11: Implement Currency Switching for Users

 

Allow users to switch between available currencies:

// Backend route to update user currency preference
app.post('/api/set-currency', async (req, res) => {
  const { currency } = req.body;
  
  // Validate currency is supported
  const supportedCurrencies = ['usd', 'eur', 'gbp', 'jpy', 'cad', 'aud'];
  if (!supportedCurrencies.includes(currency)) {
    return res.status(400).json({ error: 'Unsupported currency' });
  }
  
  // Update user preference in database
  await YourDatabase.updateUser(req.user.id, { preferredCurrency: currency });
  
  // Update session if needed
  req.session.currency = currency;
  
  return res.json({ success: true, currency });
});

// Frontend currency selector component
/\*
function CurrencySelector({ currentCurrency, onCurrencyChange }) {
  const currencies = [
    { code: 'usd', symbol: '$', name: 'US Dollar' },
    { code: 'eur', symbol: '€', name: 'Euro' },
    { code: 'gbp', symbol: '£', name: 'British Pound' },
    // Add more currencies as needed
  ];
  
  return (
    
  );
}
\*/

 

Step 12: Handle Subscriptions with Multiple Currencies

 

For recurring subscriptions, handle the currency selection at signup:

async function createSubscription(customerId, priceId, currency) {
  try {
    // Create the subscription
    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ price: priceId }],
      payment_behavior: 'default_incomplete',
      payment_settings: { save_default_payment_method: 'on\_subscription' },
      expand: ['latest_invoice.payment_intent'],
    });
    
    // Store subscription details with currency information
    await YourDatabase.saveSubscription({
      customerId: customerId,
      subscriptionId: subscription.id,
      currency: currency,
      priceId: priceId,
      status: subscription.status
    });
    
    return {
      subscriptionId: subscription.id,
      clientSecret: subscription.latest_invoice.payment_intent.client\_secret,
    };
  } catch (error) {
    console.error('Subscription creation failed:', error);
    throw error;
  }
}

 

Step 13: Handle Currency Changes in Subscriptions

 

If a user wants to change the currency of their subscription:

async function changeSubscriptionCurrency(subscriptionId, newCurrency, productId) {
  // Fetch the product to get the price ID for the new currency
  const product = await YourDatabase.findProduct(productId);
  const newPriceId = product.prices[newCurrency].priceId;
  
  // You cannot directly change the currency of a subscription in Stripe
  // Instead, you need to cancel the old one and create a new one
  
  // 1. Retrieve the current subscription
  const currentSubscription = await stripe.subscriptions.retrieve(subscriptionId);
  const customerId = currentSubscription.customer;
  
  // 2. Cancel the current subscription at period end
  await stripe.subscriptions.update(subscriptionId, {
    cancel_at_period\_end: true,
  });
  
  // 3. Create a new subscription in the new currency
  // (ideally schedule this to start when the old one ends)
  const endDate = new Date(currentSubscription.current_period_end \* 1000);
  
  // 4. Create the new subscription with the new currency price
  const newSubscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: newPriceId }],
    trial_end: currentSubscription.current_period\_end,
    payment_behavior: 'default_incomplete',
    payment_settings: { save_default_payment_method: 'on\_subscription' },
    expand: ['latest_invoice.payment_intent'],
  });
  
  // 5. Update your database records
  await YourDatabase.updateSubscriptionCurrency({
    oldSubscriptionId: subscriptionId,
    newSubscriptionId: newSubscription.id,
    newCurrency: newCurrency,
    effectiveDate: endDate
  });
  
  return {
    oldSubscriptionId: subscriptionId,
    newSubscriptionId: newSubscription.id,
    clientSecret: newSubscription.latest_invoice.payment_intent.client\_secret,
  };
}

 

Step 14: Implement Exchange Rate Handling

 

For accurate reporting and conversions, implement an exchange rate service:

// Utility to fetch current exchange rates
class ExchangeRateService {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://api.exchangerate.host/latest';
    this.cacheTimeout = 3600000; // 1 hour in milliseconds
    this.rateCache = {
      timestamp: 0,
      rates: {}
    };
  }
  
  async getRates(baseCurrency = 'USD') {
    // Check if cache is still valid
    const now = Date.now();
    if (now - this.rateCache.timestamp < this.cacheTimeout && 
        this.rateCache.baseCurrency === baseCurrency) {
      return this.rateCache.rates;
    }
    
    // Fetch new rates
    try {
      const response = await fetch(`${this.baseUrl}?base=${baseCurrency}`);
      const data = await response.json();
      
      if (data && data.rates) {
        // Update cache
        this.rateCache = {
          timestamp: now,
          baseCurrency: baseCurrency,
          rates: data.rates
        };
        return data.rates;
      }
      throw new Error('Invalid response from exchange rate API');
    } catch (error) {
      console.error('Failed to fetch exchange rates:', error);
      // If cache exists but expired, still use it as fallback
      if (this.rateCache.rates && Object.keys(this.rateCache.rates).length) {
        return this.rateCache.rates;
      }
      throw error;
    }
  }
  
  async convert(amount, fromCurrency, toCurrency) {
    const rates = await this.getRates(fromCurrency.toUpperCase());
    const rate = rates[toCurrency.toUpperCase()];
    
    if (!rate) {
      throw new Error(`Exchange rate not available for ${fromCurrency} to ${toCurrency}`);
    }
    
    return amount \* rate;
  }
}

// Usage example
const exchangeService = new ExchangeRateService('your_api_key_if_needed');
const amountInUSD = await exchangeService.convert(100, 'EUR', 'USD');

 

Step 15: Implement Comprehensive Testing

 

Test your multi-currency implementation thoroughly:

// Example test for currency conversion using Jest
test('Currency conversion should work correctly', async () => {
  // Mock the exchange rate API
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({
        rates: {
          USD: 1.1,
          GBP: 0.85,
          JPY: 130
        }
      })
    })
  );
  
  const exchangeService = new ExchangeRateService('test\_key');
  
  // Test EUR to USD conversion
  const usdAmount = await exchangeService.convert(100, 'EUR', 'USD');
  expect(usdAmount).toBeCloseTo(110, 2);
  
  // Test EUR to GBP conversion
  const gbpAmount = await exchangeService.convert(100, 'EUR', 'GBP');
  expect(gbpAmount).toBeCloseTo(85, 2);
});

// Test creating a payment intent with different currencies
test('Creating payment intent with different currencies', async () => {
  // Mock your database and Stripe API calls
  YourDatabase.findProduct = jest.fn().mockResolvedValue({
    id: 'prod\_123',
    prices: {
      usd: { priceId: 'price_usd_123', amount: 1500 },
      eur: { priceId: 'price_eur_123', amount: 1300 }
    }
  });
  
  stripe.paymentIntents.create = jest.fn().mockImplementation(
    (params) => Promise.resolve({
      id: 'pi\_test123',
      amount: params.amount,
      currency: params.currency,
      client_secret: 'test_secret'
    })
  );
  
  // Test USD payment
  const usdPayment = await createPayment('prod\_123', 'usd');
  expect(usdPayment.amount).toBe(1500);
  expect(usdPayment.currency).toBe('usd');
  
  // Test EUR payment
  const eurPayment = await createPayment('prod\_123', 'eur');
  expect(eurPayment.amount).toBe(1300);
  expect(eurPayment.currency).toBe('eur');
  
  // Test unsupported currency
  await expect(createPayment('prod\_123', 'xyz')).rejects.toThrow();
});

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022