Multi-Currency Checkout for Shopify: A Complete Developer Guide for 2026
If you're building a Shopify store — or an app that serves one — multi-currency checkout is no longer a nice-to-have. International shoppers expect to see prices in their own currency from the product page all the way through to the final "Pay now" button. When that experience breaks, cart abandonment climbs, chargebacks creep up, and support tickets pile on. This guide walks through how Shopify's multi-currency system actually works under the hood, where its native tooling stops short, and how to wire in a dedicated exchange rate API like Finexly to fill the gaps.
Whether you're building a headless storefront, a custom Shopify app, or just tightening up a Liquid theme on a mature Shopify Plus store, this guide covers the architecture decisions, API integrations, and edge cases that separate a working multi-currency checkout from one that silently bleeds revenue.
Why Multi-Currency Checkout Matters for Shopify Stores in 2026
Cross-border e-commerce now accounts for a meaningful share of all online retail. Shoppers who land on a product priced in a foreign currency — or worse, priced in their currency but with sloppy conversion rates — disproportionately bounce before checkout. The friction isn't just psychological. Foreign-currency card transactions often trigger bank-side conversion fees, surprise totals at checkout, and even fraud flags.
Shopify's own merchant research and a wide body of conversion studies consistently show the same pattern: stores that present prices in the shopper's local currency, respect local rounding conventions, and charge in that currency at checkout convert materially better than single-currency stores. The technical lift used to be significant — today it's a handful of API calls and a clean rate-refresh strategy.
Before diving into code, it's worth understanding a distinction that trips up most developers the first time they touch Shopify's multi-currency APIs.
Understanding Shop Currency vs. Presentment Currency
Shopify's entire multi-currency model hinges on two concepts:
- Shop currency — the base currency the merchant uses for pricing, reporting, and payouts. Every product is priced in this currency internally.
- Presentment currency — the currency the shopper actually sees on the storefront, in the cart, and at checkout.
When a customer in Germany browses a store whose shop currency is USD, the product might display as €92.50 (presentment currency) while Shopify internally tracks the sale at $99.00 (shop currency). Orders, refunds, and transactions are stored with both values. In the GraphQL Admin API, you'll see these as shopMoney and presentmentMoney fields on every money-valued object — line items, refunds, transactions, shipping.
The common mistake is assuming a price you fetch from the API is always in one or the other. It isn't. As of the latest API versions, all read requests in the Refund API default to presentment currency, and if you create refund transactions on a multi-currency order without explicitly passing the currency field, the call will return an error. Treat this as a hard rule: always read the currency code attached to a money value, never assume.
Now that the vocabulary is locked in, let's look at what Shopify gives you out of the box.
Shopify's Native Multi-Currency Options (and Where They Fall Short)
Shopify ships two headline features for multi-currency commerce:
1. Shopify Payments with automatic conversion. If your store is eligible for Shopify Payments, you can enable multiple presentment currencies and Shopify will auto-convert prices using rates refreshed roughly every 15 minutes from a third-party feed. It applies a small conversion fee (typically 1.5–2% depending on region) and handles rounding per currency.
2. Manual exchange rates via Shopify Markets. For merchants who want control — or who aren't on Shopify Payments — Shopify Markets lets you set your own per-currency rates. You define them, and Shopify uses the formula:
converted_price = (product_price × conversion_rate) × (1 + conversion_fee)Then rounding rules are applied per currency.
These options cover the basics. But they fall short in several real-world scenarios:
- Headless storefronts. If you're building with the Storefront API, Hydrogen, or a custom frontend, you control how prices are displayed — and you often need richer rate data than Shopify exposes (e.g., historical rates for analytics, cross-rates for currencies Shopify doesn't directly support, or rates for non-payment use cases like supplier invoicing).
- Non-Shopify Payments merchants. Stores using Stripe, PayPal, Adyen, or a local processor don't get the automatic conversion pipeline and must manage rates themselves.
- Custom pricing logic. Marketplaces, B2B stores with tiered pricing, and stores that run localized promotions often need to compute prices dynamically from a source of truth that isn't Shopify.
- Apps that act on orders post-checkout. Accounting sync apps, fraud tools, loyalty systems, and marketplace aggregators need to normalize multi-currency orders back to a common reporting currency — which means reliable historical rates at the exact timestamp of each order.
In all of these cases, you need a dedicated exchange rate API alongside Shopify.
When to Use an External Exchange Rate API
A good rule of thumb: if any of the following are true, plug in an external rates API like Finexly:
- You operate a headless or hybrid storefront and need programmatic access to live rates.
- You support currencies Shopify Payments doesn't auto-convert for your region.
- You build apps that process historical orders and need the exact rate at order time for reconciliation or audit.
- You want to display prices in currencies without committing to charging in them (e.g., "€" shown for browsing convenience while charging in USD).
- You need rate refresh rates faster than Shopify's 15-minute cadence — for example, during high-volatility periods around central bank announcements.
For deeper background on picking a provider, we've written a comparison of currency APIs and a piece specifically on REST vs WebSocket delivery patterns for FX data.
Finexly gives you real-time and historical exchange rates for 170+ currencies over a clean REST interface, with no hidden conversion fees baked into the rates and no per-call pricing surprises.
Building a Multi-Currency Checkout with Finexly and Shopify
Here's the minimum architecture for a reliable multi-currency checkout:
- Detect the shopper's currency — from geolocation, browser locale, or an explicit selector.
- Fetch the current rate from Finexly (cached at the edge, refreshed every few minutes).
- Convert prices for display — apply your rounding and margin rules.
- At checkout, use Shopify's presentment currency parameter so the order is created with the correct currency attached.
- Store the rate used as order metadata for reconciliation.
Step 1: Fetch the current rate from Finexly
The simplest call returns rates against a base currency. Here's the Node.js version:
// Fetch live rates from Finexly
async function getRates(base = 'USD') {
const url = `https://api.finexly.com/v1/latest?base=${base}&apikey=${process.env.FINEXLY_KEY}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Finexly error: ${res.status}`);
const data = await res.json();
return data.rates; // { EUR: 0.9234, GBP: 0.7891, JPY: 151.42, ... }
}Python works the same way:
import os, requests
def get_rates(base="USD"):
url = "https://api.finexly.com/v1/latest"
params = {"base": base, "apikey": os.environ["FINEXLY_KEY"]}
r = requests.get(url, params=params, timeout=5)
r.raise_for_status()
return r.json()["rates"]Or the cURL version for a quick sanity check:
curl "https://api.finexly.com/v1/latest?base=USD&apikey=YOUR_KEY"In production, wrap this in a cache layer — Redis, Cloudflare KV, or even just an in-process cache with a 60–300 second TTL. Our guide on caching and error handling best practices covers the exact patterns we recommend.
Step 2: Convert product prices in your storefront
Once you have rates, converting a Shopify price is straightforward:
function convertPrice(amountInBase, rate, roundingRule = 0.99) {
const raw = amountInBase * rate;
// Round to a "psychological" price point
return Math.floor(raw) + roundingRule;
}
// Example: $49.00 USD product, target EUR
const rates = await getRates('USD');
const eurPrice = convertPrice(49.00, rates.EUR, 0.99);
// => e.g., 45.99Step 3: Hand off to Shopify checkout in the right currency
If you're using Shopify's Storefront API for checkout, pass the presentment currency when creating the cart. With the GraphQL Cart API, the buyerIdentity field — specifically countryCode — drives which presentment currency gets applied to the cart. A simplified mutation looks like this:
mutation CreateCart {
cartCreate(input: {
buyerIdentity: { countryCode: DE }
lines: [{ merchandiseId: "gid://shopify/ProductVariant/123", quantity: 1 }]
}) {
cart { id checkoutUrl
cost { totalAmount { amount currencyCode } }
}
}
}Shopify then surfaces prices in the appropriate presentment currency for that country, and the checkout URL you redirect the shopper to will charge in that currency — assuming your payment gateway is configured for it.
If you're on a classic (non-headless) storefront, you don't create carts via API. Instead, you can use Shopify's Geolocation app or set the currency via the ?currency=EUR URL parameter. Liquid filters like {{ product.price | money }} will then respect the active presentment currency.
Step 4: Store the rate used with each order
This is the step most integrations skip — and regret later. When an order is placed, write the Finexly rate you used (and the timestamp) to order metafields:
// After checkout, attach rate metadata
await admin.graphql(`
mutation AddRateMetafield($orderId: ID!, $rate: String!, $ts: String!) {
orderUpdate(input: {
id: $orderId
metafields: [
{ namespace: "fx", key: "rate", value: $rate, type: "single_line_text_field" }
{ namespace: "fx", key: "rate_timestamp", value: $ts, type: "single_line_text_field" }
{ namespace: "fx", key: "source", value: "finexly", type: "single_line_text_field" }
]
}) { order { id } userErrors { field message } }
}
`, { variables: { orderId, rate: String(rates.EUR), ts: new Date().toISOString() } });Future-you will thank present-you when an accountant asks why the EUR total on order #10042 doesn't match the raw USD × current rate three months later.
Displaying Converted Prices: Liquid vs Headless
Liquid (classic themes)
Shopify provides the money family of filters that respect the active presentment currency automatically when Shopify Payments multi-currency is enabled:
<!-- Respects presentment currency -->
<p class="price">{{ product.price | money }}</p>
<!-- Explicit formatting -->
<p class="price">{{ product.price | money_with_currency }}</p>If you're using manual rates (not Shopify Payments) or you want to layer Finexly rates on top for extra currencies, inject rates via a small JavaScript snippet in theme.liquid:
<script>
window.__FX__ = {{ shop.currency | json }};
fetch('/apps/fx/rates').then(r => r.json()).then(rates => {
document.querySelectorAll('[data-price-cents]').forEach(el => {
const cents = parseInt(el.dataset.priceCents, 10);
const target = document.documentElement.lang.split('-')[1] || 'US';
const fx = rates[target];
if (fx) el.textContent = new Intl.NumberFormat(
document.documentElement.lang,
{ style: 'currency', currency: fx.currency }
).format((cents / 100) * fx.rate);
});
});
</script>Proxy /apps/fx/rates through a Shopify App Proxy to your backend, which calls Finexly with caching in front.
Headless (Hydrogen, Next.js, custom)
In a headless setup you have full control. A typical Next.js pattern:
// app/lib/fx.ts - server-side rate fetcher with caching
export async function getFxRates(base = 'USD') {
const res = await fetch(
`https://api.finexly.com/v1/latest?base=${base}&apikey=${process.env.FINEXLY_KEY}`,
{ next: { revalidate: 120 } } // 2-minute ISR cache
);
return res.json();
}
// Product page - server component
export default async function Product({ params }) {
const [product, fx] = await Promise.all([
getProduct(params.handle),
getFxRates('USD'),
]);
const userCurrency = getUserCurrency(); // from headers, cookie, etc.
const displayPrice = product.priceUsd * fx.rates[userCurrency];
return <ProductView product={product} price={displayPrice} currency={userCurrency} />;
}See our Next.js currency converter tutorial for a deeper walkthrough of the caching and revalidation patterns.
Handling the Hard Parts: Rounding, Fees, and Rate Freshness
Three operational details separate a polished multi-currency checkout from a broken one.
Rounding rules. Japanese yen doesn't have decimals. Swiss francs traditionally round to the nearest 0.05. Customers in most countries expect prices ending in .99 or .95. Shopify's built-in rounding rules handle the basics, but if you're converting outside Shopify Payments, implement explicit rules per currency:
const ROUNDING = {
JPY: (v) => Math.round(v), // no decimals
CHF: (v) => Math.round(v * 20) / 20, // nearest 0.05
default: (v) => Math.floor(v) + 0.99, // X.99
};
function round(value, currency) {
return (ROUNDING[currency] || ROUNDING.default)(value);
}Conversion margin. If you're not using Shopify Payments' built-in fee, bake a small margin (typically 1–3%) into your displayed rate to cover the spread your payment processor charges. Make this explicit and configurable per market — don't hard-code it.
Rate freshness. Rates move. During central bank announcements or major macro events, rates can shift 1–2% in minutes. For high-AOV stores, a stale rate is a loss. Finexly updates rates every 60 seconds for supported currencies; combined with a 2-minute edge cache, that gives you near-live pricing without hammering the API. For context on why this matters, see our piece on handling currency volatility.
Tracking and Reconciling Multi-Currency Orders
When finance asks "how much did we actually make in USD last quarter?", the answer depends on which rate you use. Three common approaches:
- Rate at order time — truest to economic reality; use the rate you stored in metafields.
- Rate at payout time — matches what hit your bank; use Shopify's payout API rates.
- Average monthly rate — smoother for accounting; pull monthly averages from Finexly's historical endpoint.
Finexly's historical rates API makes all three straightforward:
# Rate on a specific date
curl "https://api.finexly.com/v1/historical/2026-03-15?base=USD&apikey=YOUR_KEY"
# Time series for averages
curl "https://api.finexly.com/v1/timeseries?start=2026-03-01&end=2026-03-31&base=USD&symbols=EUR&apikey=YOUR_KEY"Most finance teams prefer approach #1 for revenue recognition and approach #2 for cash reconciliation. Store both.
Common Pitfalls (and How to Avoid Them)
- Caching rates forever. A 24-hour cache on exchange rates is too long for a store that takes orders during Asian and European market hours. Cap TTLs at 5–10 minutes, and refresh on cache miss with a circuit breaker.
- Trusting the client. Never compute final prices in JavaScript alone — a hostile client can manipulate them. Re-verify on the server or in a Shopify Function before order creation.
- Ignoring rounding on line items vs totals. Round at the line-item level, not just the total, to avoid "total doesn't match sum of items" display bugs in Liquid.
- Forgetting refunds. When you refund a multi-currency order, you must pass the
currencyfield explicitly or the Shopify API will reject the call. The refund currency must match the original presentment currency. - Skipping observability. Log every rate fetch with its source, timestamp, and response time. When a rate looks wrong, you need an audit trail.
Frequently Asked Questions
Does Shopify Payments support every currency? No. Shopify Payments supports a growing but finite list of presentment currencies, and availability depends on the merchant's region. For unsupported currencies, you need to either route those markets through a different processor or display conversions without charging in the target currency.
Can I use Finexly to drive Shopify's conversion rates directly? Not directly — Shopify Payments uses its own internal rate source. What you can do is feed Finexly rates into the Markets manual rates setting via the Admin API, which effectively replaces Shopify's auto-rates with yours for stores on manual pricing.
How often should I refresh exchange rates? For most storefronts, a 2–5 minute edge cache on top of Finexly's per-minute updates is the sweet spot. It keeps prices fresh without creating load. For high-AOV or low-margin products, go tighter (30–60 seconds); for low-AOV fast-moving goods, you can stretch to 15 minutes.
Do I need a multi-currency API if I use Shopify Payments with automatic conversion? Often, yes — for everything adjacent to checkout. Accounting reconciliation, marketing analytics, supplier invoicing, and price displays in markets Shopify doesn't cover all benefit from a dedicated rates API. See our currency API for accounting software integration guide for the reconciliation side.
What's the best way to test multi-currency checkout? Use Shopify's Bogus Gateway in a development store, switch your IP via VPN to simulate different country contexts, and verify that the order confirmation, receipt, and Admin all show matching presentment amounts. Then test a refund — that's where most integrations silently break.
Ship Faster with Finexly
Multi-currency checkout on Shopify is one of those features that looks simple from the outside and has a long tail of edge cases inside. The good news: with a solid exchange rate API underpinning your integration, most of those edge cases reduce to well-understood patterns — cache, round, store, reconcile.
Ready to integrate reliable exchange rates into your Shopify store or app? Get your free Finexly API key — no credit card required. Start with 1,000 free requests per month, scale up as your store grows, and ship a multi-currency checkout your international customers actually enjoy using.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →