
A pricing page in the wrong currency quietly costs signups. An international visitor lands, sees a number in a foreign currency, does the mental math wrong or not at all, and leaves. Showing prices in the visitor's own currency is one of the cheapest conversion wins a SaaS has, and it's also one of the easiest features to ship badly. The naive version demos fine and breaks in production: stale rates, rounding that produces ugly prices, invoices that don't reconcile, refunds issued at the wrong rate.
Here's the stack that avoids those traps.
The first attempt is usually a hardcoded table of rates copied from a search result. It works until the rates drift, which takes days. The second attempt fetches rates from some free endpoint once and caches them forever, which is the same problem with extra steps. Currency rates move continuously on weekdays and sit still on weekends, so any system that treats them as constants slowly diverges from reality, and the gap shows up as customers being charged a little too much or too little.
Three things actually have to be handled: where the rates come from, how often they refresh, and which rate is the source of truth once money moves. Get those right and the rest is formatting.
Before any conversion happens, the product has to decide which currency to display. Two signals drive that choice. An explicit account setting is the reliable one, and the only signal worth trusting once a customer has stated where they are. IP geolocation is a reasonable guess for a first-time anonymous visitor and nothing more. The common mistake is locking a visitor into a geolocated currency with no way to change it, which frustrates travelers, expats, and anyone behind a VPN. The safe default is to guess with geolocation, always show a visible control to switch, and remember the choice for next time.
Exchange-rate data has to come from a source that updates continuously and exposes history, not a one-off scrape that goes stale. OANDA, long known as a source of currency-exchange data, exposes live and historical currency prices through its US Forex API the pricing endpoint returns the current bid, ask, and mid rate for a currency pair, and the candles endpoint returns historical rates at whatever granularity a report needs. A single authenticated request returns the current number:
The mid rate, the average of bid and ask, is the fair market number to show a customer. The spread between bid and ask matters when money is actually being exchanged, but for displaying a localized price, the mid is the honest figure to convert with.
Calling a rates API on every page load is slow and pointless. Exchange rates don't move enough minute to minute to change a pricing page, so the right pattern is to cache a snapshot and refresh it on a schedule. Once an hour covers almost everything; once a day is fine for many products.
Two edge cases are worth handling up front. Currency markets close over the weekend, so a Friday rate is supposed to persist until Monday, and the system shouldn't treat a rate that stops changing as a bug. And any fetch can fail, so the last good rate needs to be stored and reused when a refresh errors out, rather than falling back to a hardcoded guess or letting the checkout page crash.
Displaying a converted price and charging in a foreign currency are two different features, and conflating them is where the support tickets come from.
For display only, convert the base price with the cached mid rate and round to something that reads like a real price instead of 18.73. Most products convert and then re-apply their own psychological pricing, rounding to the nearest .99 or whole unit, so the localized page still looks deliberate rather than machine-generated.
For actually charging in the customer's currency, the payment processor, whether Stripe, Paddle, or another, performs the settlement conversion at its own rate, which differs slightly from any display rate. The clean division of labor is to let the processor own the charge currency and treat the rates API as the source for display prices and internal estimates, not as the system of record for money that has already moved. When a transaction does happen, the rate used at that instant should be written onto the record, so refunds, invoices, and financial reports reference the historical rate from that day instead of today's.
Multi-currency support looks like a one-line conversion and turns out to be a small system: a reliable rate source, a sensible refresh-and-cache layer, careful rounding, and a clear rule about which rate wins once money moves. None of the pieces are hard, but skipping any one of them is what turns a clean conversion win into a stream of tickets about prices that don't match. Built properly, it's one of the higher-leverage afternoons a founder selling to an international audience can spend.