A shopper in Zurich who sees “$49.00” has to do mental math before buying – and many won’t. A multi-currency pricing API solves that: it gives your store current market exchange rates so you can show “CHF 44.90” instead. fxapi provides rates for 190+ currencies through a single HTTPS endpoint, with any currency as the base. This page shows how to turn those rates into localized catalog prices, round them to charm prices, and lock the rate at checkout so every order is traceable.
Why currency conversion for e-commerce is more than a multiplication
Multiplying a USD price by a rate takes one line of code. Running it in production raises more questions:
- Display vs. settlement currency. The shopper sees and pays in the display currency (EUR, GBP, JPY). Your payment provider settles to you in your settlement currency, often at its own conversion rate. Those two numbers will not match exactly, and finance needs to see both.
- Rate freshness. Prices that change every minute confuse shoppers; prices refreshed once a month expose you to currency moves. You need a deliberate refresh cadence.
- Rounding. 44.8731 CHF is not a price anyone writes on a shelf. Each currency has its own number of decimals and its own pricing conventions.
- Consistency. The price on the product page, in the cart and at payment must be identical, even if a new rate arrives in between.
- Auditability. Six months later someone will ask which rate produced a given order total.
How fxapi works as a multi-currency pricing API
| Endpoint | Role in a store | Plan |
|---|---|---|
/v1/latest | One call returns every rate you need relative to your base currency | All plans |
/v1/currencies | decimal_digits, symbol, symbol_native and countries per currency for formatting | All plans |
/v1/convert | Converted amount for a single value, optionally on a past date | Basic and up |
/v1/historical | End-of-day rates for a past date, for refunds and disputes | All plans |
How fresh your prices can be depends on the plan: Free updates daily, Basic hourly, and Professional and Enterprise every 60 seconds. Every response carries meta.last_updated_at, the UTC timestamp of the data. Compare limits on the pricing page.
A quick quota check: refreshing once an hour costs about 744 requests per month, which fits Basic (15,000). Refreshing every minute costs about 44,640, which fits Professional (600,000). A daily refresh uses around 31 requests and stays inside the Free plan’s 300.
Architecture: from base price to locked checkout rate
- Keep one source of truth. Store product prices in your base currency (for example USD) in your catalog.
- Refresh rates on a schedule. A server-side job calls
/v1/latest?base_currency=USD¤cies=EUR,GBP,CHF,JPY,CADat your plan’s cadence and writes the result pluslast_updated_atto a cache (Redis, a database row, or memory). Details in caching exchange rates. - Convert and round at render time. Multiply the base price by the cached rate, then apply the currency’s rounding rule. Cache the rounded price list per currency if your catalog is large.
- Snapshot the rate into the cart. When an item enters the cart, store the rate and its timestamp with the cart. The cart total must not move if a new rate arrives while the shopper is browsing.
- Lock at checkout. Create the order with
display_currency,display_amount,base_amount,rate,rate_timestampand a rate source label. Charge the payment provider the display amount. - Record settlement. When the provider reports the settled amount in your settlement currency, store it next to the locked rate. The difference is your conversion cost or gain.
Code example: a Node.js price service
The example below uses plain fetch with the API key in the apikey header. It refreshes rates hourly, rounds to charm prices and returns a quote you can lock into an order.
const BASE = "USD";
const TARGETS = ["EUR", "GBP", "CHF", "JPY", "CAD"];
const DECIMALS = { EUR: 2, GBP: 2, CHF: 2, JPY: 0, CAD: 2 }; // from /v1/currencies
let snapshot = null; // { rates, lastUpdatedAt }
async function refreshRates() {
const url = new URL("https://api.fxapi.com/v1/latest");
url.searchParams.set("base_currency", BASE);
url.searchParams.set("currencies", TARGETS.join(","));
const res = await fetch(url, { headers: { apikey: process.env.FXAPI_KEY } });
if (!res.ok) {
if (snapshot) return; // keep serving the last good rates
throw new Error(`fxapi error ${res.status}`);
}
const { meta, data } = await res.json();
snapshot = {
rates: Object.fromEntries(Object.entries(data).map(([code, r]) => [code, r.value])),
lastUpdatedAt: meta.last_updated_at,
};
}
function charmPrice(amount, currency) {
if (DECIMALS[currency] === 0) {
return Math.ceil(amount / 100) * 100 - 20; // 4,431 JPY -> 4,480
}
return Math.round((Math.ceil(amount) - 0.01) * 100) / 100; // 44.87 CHF -> 44.99
}
export function quote(basePrice, currency) {
const rate = snapshot.rates[currency];
return {
currency,
amount: charmPrice(basePrice * rate, currency),
baseCurrency: BASE,
baseAmount: basePrice,
rate,
rateTimestamp: snapshot.lastUpdatedAt, // persist this with the order
};
}
await refreshRates();
setInterval(refreshRates, 60 * 60 * 1000); // hourly = Basic plan cadence
Persist the object returned by quote() on the cart line and copy it into the order at checkout. More language examples: JavaScript guide and Python guide.
Rounding to charm prices
Rounding rules are a merchandising decision, but they must be applied per currency:
| Currency | decimal_digits | Example rule | Example |
|---|---|---|---|
| EUR, CHF, GBP | 2 | Round up, end in .99 or .90 | 44.87 → 44.99 |
| JPY | 0 | Round up to the next 100, end in 80 | 4,431 → 4,480 |
| KRW | 0 | Round up to the next 1,000 | 58,640 → 59,000 |
Read decimal_digits and rounding from /v1/currencies instead of hard-coding them. Store money as integers in minor units (cents) to avoid floating-point drift, as explained in currency rounding and precision.
Pitfalls and best practices
- Never ship the API key to the browser. Refresh rates on your server and send converted prices to the storefront.
- Don’t reprice an open cart. Lock the rate when the item is added, or show a clear message if the cart total changes.
- Store the rate and timestamp on every order.
rateplusmeta.last_updated_atlets finance reproduce any total. For refunds or disputes, look up the original date with the historical exchange rates API. - Add a buffer if you absorb FX risk. Many stores add a small percentage on top of the mid-market rate to cover payment-provider conversion fees.
- Request only the currencies you sell in.
currencies=EUR,GBP,CHFkeeps responses small. - Fail over to the last good rate. If a refresh fails, keep serving the cached rates and alert on staleness rather than breaking checkout.
- Watch quota headers.
X-RateLimit-Remaining-Quota-Monthtells you how much headroom is left; HTTP 429 means a limit was reached.
Custom store integrations
Platforms with custom app or plugin systems – Shopify-style apps, WooCommerce-style PHP hooks, headless commerce backends – all follow the pattern above: a scheduled job writes rates to storage, and a price filter or pricing hook converts and rounds at render time. fxapi is a plain REST API, so it fits any stack that can make an HTTPS request; there is no platform-specific plugin to install.
Start pricing in your shoppers’ currencies
The Free plan’s daily rates are enough to prototype localized prices today. When you need hourly refresh and the currency converter API, Basic costs $9.99 per month; Professional adds 60-second updates for fast-moving catalogs. Create a free API key, call /v1/latest with your base currency, and ship your first localized price list this week.
Everything on this page runs on the fxapi foreign exchange rates API: one key, 190+ currencies, data back to 1999.
Frequently asked questions
How often should an online store refresh its exchange rates?
Should I call the convert endpoint for every product price?
/v1/latest and multiply locally. One request covers your entire catalog in every currency; /v1/convert is better suited to one-off amounts.What is the difference between display currency and settlement currency?
How do I round converted prices to values like 19.99?
decimal_digits from /v1/currencies: for example round up and subtract 0.01 for EUR, or round to the nearest 100 and end in 80 for JPY. See currency rounding and precision.