Home Documentation Playground Pricing API Status Blog About FAQ Support

How to Build a Multi-Currency Checkout for Shopify

Reviewed by Madhushan, Fintech Developer — May 2026

The hardest thing about multi-currency on Shopify is not the code — it is deciding how much code you actually need. Most stores reach for a third-party app and end up paying for features they will never use; others write a custom proxy and discover three months later that Shopify Payments already does most of it.

This guide walks through the two real options, the trade-offs that decide between them, and the implementation details for each. By the end you should know which path your store should take and how to ship it without breaking accounting.

What you will build: A Shopify storefront that detects the visitor's currency, shows localized prices on product pages and the cart, lets the customer pay in their own currency, and writes the locked-in exchange rate to the order so refunds and reporting stay clean.

The two real options

  Shopify Markets (built-in) Custom rates with your own API
Setup timeUnder 30 minutesHalf a day to a week
Currencies130+ supported by Shopify PaymentsAny currency you can fetch a rate for
Default FX ratesShopify-provided (with retail markup baked in)Mid-market rates from your chosen API
SettlementAuto-converted to store currencyYou handle settlement (usually via Shopify Payments still)
Best forMost storesStores that need transparent rates, custom markets, or one rate source across systems

If you do not have a strong reason to go custom, start with Shopify Markets. The rest of this article covers both, and the second half is what you actually need when "the built-in option" is not enough.

Option 1: Shopify Markets (the easy path)

Shopify Markets is the modern multi-currency primitive on Shopify. It replaces the older "International Pricing" UI and gives you per-market currency settings, prices, domains, and shipping rules.

Step 1: Enable the markets you ship to

  1. Open Settings > Markets in your Shopify admin.
  2. Either turn on the default "International" market (covers everywhere not handled by another market) or create a specific market per region: EU, UK, North America, etc.
  3. For each market, choose a currency. Shopify Payments must support that currency for it to work at checkout.

Step 2: Pick a rate source per market

For each market's currency, you have three rate options:

Step 3: Show the right prices in the storefront

Modern Shopify themes (Dawn, Sense, etc.) handle this automatically when Markets is enabled — the price in Liquid resolves to the customer's market currency. If your theme is older, you may need to swap {{ product.price | money }} for {{ product.price | money_with_currency }} so the visible currency code matches what the customer will be charged.

<!-- product-price.liquid -->
<span class="price">
  {{ product.price | money_with_currency }}
</span>

Add a currency selector if you want customers to be able to override the auto-detected market:

{% form 'localization' %}
  <select name="currency_code">
    {% for currency in localization.available_countries %}
      <option value="{{ currency.currency.iso_code }}"
              {% if currency.currency.iso_code == localization.country.currency.iso_code %}selected{% endif %}>
        {{ currency.currency.iso_code }} {{ currency.currency.symbol }}
      </option>
    {% endfor %}
  </select>
  <button type="submit">Update</button>
{% endform %}

What you get out of the box

For most stores doing < $5M/year, this is the whole job. Stop here.

Option 2: Custom rates with your own exchange rate API

The reasons to go custom are specific:

Architecture

The shape of a custom flow has three pieces:

  1. A rate sync. A scheduled job (Cloudflare Worker, Vercel Cron, AWS Lambda, anything cheap) hits the AllRatesToday API every few minutes and writes the rates somewhere your storefront can read fast — KV, Redis, or just a JSON blob behind a CDN.
  2. A storefront overlay. A small script on the theme reads the cached rates, picks a target currency for the visitor, and rewrites prices in the DOM (or via a Section Rendering API call). It writes the chosen currency to a cookie so it persists.
  3. A checkout adapter. Either a Shopify Markets manual rate that you push into the admin via the GraphQL API on a schedule, or a custom checkout extension that reads the rate at checkout time and writes the source/target amounts onto the order metadata.

Step 1: Sync rates from AllRatesToday

The simplest sync is a Cloudflare Worker on a 5-minute cron that fetches a basket of currencies and writes them to KV. KV reads are sub-millisecond from the edge, so the storefront pays no latency for them.

// worker.ts
const BASE = 'USD'; // your store currency
const TARGETS = ['EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'INR', 'BRL', 'MXN'];

export default {
  async scheduled(_event: ScheduledEvent, env: Env) {
    const rates: Record<string, number> = {};

    for (const target of TARGETS) {
      const res = await fetch(
        `https://allratestoday.com/api/v1/rates?source=${BASE}&target=${target}`,
        { headers: { Authorization: `Bearer ${env.ALLRATESTODAY_KEY}` } }
      );
      if (!res.ok) continue;
      const data = await res.json();
      rates[target] = data.rate;
    }

    rates[BASE] = 1;
    await env.RATES.put('latest', JSON.stringify({
      base: BASE,
      rates,
      fetchedAt: new Date().toISOString(),
    }));
  },

  async fetch(_req: Request, env: Env) {
    const cached = await env.RATES.get('latest');
    return new Response(cached, {
      headers: {
        'content-type': 'application/json',
        'cache-control': 'public, max-age=60',
        'access-control-allow-origin': '*',
      },
    });
  },
};

Schedule the cron in wrangler.toml, deploy, and you have a public endpoint your storefront can hit on every page load with effectively no cost.

Step 2: Display converted prices in the theme

In your theme's theme.liquid or a dedicated section, render the base prices and let a small script localize them. Shopify already exposes the cents amount on each product element, so use that as the source of truth.

<script>
(async function () {
  const FX_ENDPOINT = 'https://your-worker.workers.dev/';
  const stored = localStorage.getItem('display_currency');
  const target = stored ?? new Intl.NumberFormat().resolvedOptions().currency ?? 'USD';

  const res = await fetch(FX_ENDPOINT);
  const { base, rates } = await res.json();
  const rate = rates[target] ?? 1;

  document.querySelectorAll('[data-price-cents]').forEach(el => {
    const cents = Number(el.dataset.priceCents);
    const converted = (cents / 100) * rate;
    el.textContent = new Intl.NumberFormat(undefined, {
      style: 'currency',
      currency: target,
    }).format(converted);
  });
})();
</script>

In your product template, render prices with the cents attribute:

<span data-price-cents="{{ product.price }}">
  {{ product.price | money_with_currency }}
</span>

The Liquid output is the fallback (no JS, search-engine indexable, server-rendered). The script swaps it client-side once rates are loaded.

Step 3: Get the right currency to checkout

This is where it gets opinionated. You have two options:

Option A: Push rates into Shopify Markets as manual rates. Use the Shopify GraphQL Admin API marketCurrencySettingsUpdate mutation to set a manual rate for each market on a schedule. Customers still check out through Shopify Payments in their currency; you have just replaced Shopify's auto rate with your own. This is the simplest path and keeps Shopify happy with PCI, refunds, and reconciliation.

mutation UpdateMarketRate($id: ID!, $rate: Decimal!) {
  marketCurrencySettingsUpdate(
    marketId: $id,
    input: {
      manualRate: $rate
    }
  ) {
    market { id }
    userErrors { field message }
  }
}

Option B: A checkout extension that calculates totals from your rate. Use Shopify Functions / Cart Transform to apply the conversion as a discount or a custom line item. This is more work and you must be careful with rounding, but it gives you full control over the math. Use it only when Option A truly cannot express what you need.

Step 4: Lock the rate on the order

Whichever option you pick, the rate at checkout time must end up on the order so refunds and accounting stay coherent.

// orders/create webhook
async function onOrderCreated(order: ShopifyOrder, env: Env) {
  const cached = await env.RATES.get('latest');
  const { rates } = JSON.parse(cached);
  const target = order.presentment_currency;
  const rate = rates[target];

  await shopify.metafields.set({
    ownerId: order.id,
    namespace: 'fx',
    key: 'rate_at_order',
    type: 'json',
    value: JSON.stringify({
      source: 'USD',
      target,
      rate,
      lockedAt: new Date().toISOString(),
    }),
  });
}

Tax, rounding, and the parts that bite later

Display rounding

Mid-market FX gives you ugly numbers like €19.4732. Round to two decimals for display (or zero for JPY/KRW), but keep the unrounded number for the math. Round once at the very end.

Psychological pricing

If your USD price is $19.99, the converted EUR will not be €19.99. Either accept this and round to nearest cent, or override per-currency in Shopify Markets so EUR shoppers see €19.99 too. Most stores eventually choose the latter for round-numbered tiers.

Tax

Tax is calculated on the presentment price by Shopify. If you change the rate, the tax adjusts automatically. Do not roll your own tax math — let Shopify handle it.

Currency symbol collisions

Hard-coding $ means CAD shoppers see USD-looking prices. Use money_with_currency in Liquid (which renders $19.99 CAD) or Intl.NumberFormat with the ISO code in JS. Symbol-only formatting belongs only in domains where the currency is unambiguous.

Common pitfalls

Do not refetch the rate at refund time. If a customer paid €18.40 in March at a rate of 1.084 and you refund in April when the rate is 1.092, you must use 1.084 (the locked rate from the order). Otherwise you owe the customer cents you cannot reconcile, and your accounting will not balance.

Frequently Asked Questions

Does Shopify support multi-currency checkout out of the box?

Yes — Shopify Markets enables checkout in 130+ currencies on Basic plans and up. Customers see localized prices, pay in their currency, and Shopify converts to your store currency for payouts. The default rates come from Shopify; you can override them with manual rates set per market.

When should I bring my own exchange rate API?

When you need transparent mid-market rates rather than the retail markup baked into Shopify's auto rate, when you want a single rate source across Shopify, your accounting system, and your invoices, or when Shopify Markets does not support a market you sell into.

How do I display converted prices on Shopify product pages?

For the native multi-currency setup, the money_with_currency Liquid filter handles it. For a custom flow, render the base price with a data-price-cents attribute and let a small script swap it client-side using rates from your API.

Can I lock the FX rate when an order is placed?

Yes. With Shopify Markets, the rate is automatically written to the order via the presentment fields. With a custom flow, capture the rate in an orders/create webhook and write it to order.metafields. Refunds and reporting must then read from those fields, not refetch a fresh rate.

What is the cheapest way to run the rate sync?

A Cloudflare Worker with a cron trigger. The Worker invocations and KV writes for a 5-minute cron sit comfortably inside the free tier, and edge-cached reads from the storefront cost nothing. Your only cost is the rate API itself, which AllRatesToday provides with a free tier for development and small production volumes.

Power Your Shopify Multi-Currency Setup

Real-time mid-market rates for 160+ currencies, refreshed every 60 seconds. Free API key, no credit card required.

Get Your Free API Key