Last updated: 5 August 2026

Every business that invoices clients in more than one currency faces the same problem: exchange rates change between the time a quote is sent and the time an invoice is paid. A system that hard-codes rates or manually updates them will eventually produce an invoice with the wrong total.

This guide walks through building an automated multi-currency invoice system backed by a live currency exchange API. By the end you will have: rate fetching with caching, line-item conversion logic, rate locking at invoice creation time, historical rate lookup for audit trails, and an optional cron job to pre-fetch daily rates.

All code uses the CurrencyFreaks API. The free plan (1,000 calls/month, SSL included) is enough to run this in production for low-to-medium invoice volumes.

How the System Works

The core flow has four steps:

  1. Customer currency is determined (from their profile, billing address, or IP detection using the CurrencyFreaks IP-to-currency endpoint)
  2. The latest rate for base currency → customer currency is fetched from the API (or served from cache)
  3. Line items and tax are converted using that rate
  4. The exact rate and timestamp are stored with the invoice record for accounting and audit purposes

Step 1 — Fetch Live Rates with Caching (Node.js)

Never call the API on every invoice render. Cache rates with a TTL matching your plan:

const axios = require('axios');
const NodeCache = require('node-cache');
const rateCache = new NodeCache({ stdTTL: 3600 }); // 1 hour — Starter plan

async function getLiveRate(baseCurrency, targetCurrency) {
  const key = `${baseCurrency}_${targetCurrency}`;
  const cached = rateCache.get(key);
  if (cached) return cached;
  const res = await axios.get('https://api.currencyfreaks.com/v2.0/rates/latest', {
    params: {
      apikey: process.env.CF_API_KEY,
      base: baseCurrency,
      symbols: targetCurrency
    },
    timeout: 8000
  });
  const rate = parseFloat(res.data.rates[targetCurrency]);
  rateCache.set(key, { rate, fetchedAt: res.data.date });
  return { rate, fetchedAt: res.data.date };
}

Step 2 — Convert Line Items

Apply conversion to individual line items, not just the total. This gives customers a transparent breakdown:

function convertInvoice(invoice, exchangeRate) {
  const { rate, fetchedAt } = exchangeRate;
  const convertedItems = invoice.lineItems.map(item => ({
    ...item,
    unitPrice: (item.unitPrice * rate).toFixed(2),
    total: (item.unitPrice * item.quantity * rate).toFixed(2)
  }));
  const subtotal = convertedItems.reduce((sum, i) => sum + parseFloat(i.total), 0);
  const tax = (subtotal * (invoice.taxRate / 100)).toFixed(2);
  const total = (subtotal + parseFloat(tax)).toFixed(2);
  return {
    ...invoice,
    lineItems: convertedItems,
    subtotal: subtotal.toFixed(2),
    tax,
    total,
    currency: invoice.customerCurrency,
    fxRate: rate,
    fxRateDate: fetchedAt   // store for audit trail
  };
}

Step 3 — Lock the Rate at Invoice Creation

Once a customer confirms an order, the exchange rate must be locked. Rate changes after confirmation should not affect the invoice total. Store the locked rate in your database alongside the invoice:

// Pseudocode — adapt to your ORM
async function createInvoice(orderData) {
  const fxData = await getLiveRate(orderData.baseCurrency, orderData.customerCurrency);
  const invoice = await db.invoices.create({
    customerId: orderData.customerId,
    lineItems: orderData.lineItems,
    baseCurrency: orderData.baseCurrency,
    customerCurrency: orderData.customerCurrency,
    fxRateUsed: fxData.rate,       // lock it here
    fxRateFetchedAt: fxData.fetchedAt,
    createdAt: new Date()
  });
  return convertInvoice({ ...orderData }, fxData);
}

Step 4 — Historical Rate Lookup for Audit and Tax

Tax authorities sometimes ask for the exchange rate that was in effect on the invoice date. The CurrencyFreaks historical endpoint lets you retrieve any past rate:

import requests
def get_historical_rate(date: str, base: str, target: str) -> float:
    res = requests.get(
        'https://api.currencyfreaks.com/v2.0/rates/historical',
        params={'apikey': os.environ['CF_API_KEY'], 'date': date, 'base': base, 'symbols': target},
        timeout=8
    )
    res.raise_for_status()
    return float(res.json()['rates'][target])

# Example: What was the USD/EUR rate on 15 March 2026?
rate = get_historical_rate('2026-03-15', 'USD', 'EUR')
print(f'USD/EUR on 2026-03-15: {rate}')

Step 5 — Detect Customer Currency from IP (Optional)

CurrencyFreaks provides a unique IP-to-currency endpoint that automatically identifies the most appropriate currency for a given IP address — useful for pre-filling the customer currency field without asking the user:

const res = await axios.get('https://api.currencyfreaks.com/v2.0/ip-to-currency', {
  params: { apikey: process.env.CF_API_KEY, ip: customerIp }
});
const suggestedCurrency = res.data.currency_code; // e.g. 'PKR', 'INR', 'GBP'

Step 6 — Automate Daily Rate Pre-Fetch with a Cron Job

For high-volume applications, pre-fetch rates on a schedule so they are always in cache when an invoice needs to be generated:

// cron: 0 * * * * (every hour)
async function prefetchRates() {
  const currencies = ['EUR', 'GBP', 'PKR', 'INR', 'CAD', 'AUD', 'SGD'];
  for (const currency of currencies) {
    await getLiveRate('USD', currency);
    console.log(`Pre-fetched USD/${currency}`);
  }
}

Best Practices

  • Always store the exact FX rate and timestamp with every invoice — never recalculate from current rates after the fact
  • Apply conversion to line items, not just the total — this keeps the invoice breakdown transparent to customers
  • Lock the rate at order confirmation, not at payment — the rate should not change between confirmation and when the PDF is generated
  • Use the historical endpoint for tax reporting — do not rely on your stored rate being the only source of truth; cross-reference with the API when audited
  • Never expose your API key in client-side JavaScript — all calls must go through your backend

FAQs

How often should I update the exchange rates?

For display purposes on product pages: hourly is sufficient. For invoice creation: always fetch a fresh rate (or a cached rate less than one hour old) at the moment the customer confirms the order.

Can I lock a rate for a customer quote that expires in 48 hours?

Yes. Store the rate at the time of quote creation and display an expiry notice ("Rate valid until [time]"). After expiry, refetch and update the quote.

Does CurrencyFreaks provide historical rates for tax reporting?

Yes. All paid plans include historical data going back to 1984. The /rates/historical endpoint accepts a date parameter in YYYY-MM-DD format.

Can I integrate this with Stripe or PayPal?

Yes. Convert the invoice total to the customer's currency using this system, then pass the converted amount and currency code to your payment gateway. Stripe and PayPal both accept ISO 4217 currency codes directly.

Sign up for your free CurrencyFreaks API key and start building your multi-currency invoice system today.