ShopifyCurrencyTutorial

Shopify Currency Widget with Live Exchange Rates (No App Required)

ApogeoAPI6 min read

Shopify's app store has dozens of "currency converter" apps — most cost $5-30/month and lock you into the vendor's theme. If your store doesn't have huge volume, the math doesn't work.

This is the no-app version: ~50 lines of JavaScript pasted into your theme's theme.liquid, using ApogeoAPI for the live exchange rates. Works on any Shopify plan including Lite, costs $0 for stores under ~5K daily visitors.

How it works

  1. JavaScript runs on every page load.
  2. Fetches the visitor's country (cached at edge).
  3. Fetches the live USD->X exchange rate.
  4. Finds all elements marked with data-shopify-price and overlays the converted amount.
  5. Original price (in shop currency) stays in the DOM but is visually replaced. Cart still uses the canonical Shopify currency for checkout.

This is display-only. Shopify still charges in your shop's base currency. Visitors see comfortable numbers; Shopify accounting stays clean.

The script (paste into theme.liquid before </body>)

<script>
(async function() {
  const APOGEO_KEY = 'pk_live_YOUR_PUBLIC_KEY'; // see "Public key" note below
  const SHOP_CURRENCY = '{{ shop.currency }}';   // Liquid: e.g. "USD"

  // Cache key per visitor (per browser, per day)
  const day = new Date().toISOString().slice(0, 10);
  const cacheKey = 'apogeo_fx_' + day;
  let geo = null;

  try {
    const cached = sessionStorage.getItem(cacheKey);
    if (cached) geo = JSON.parse(cached);
  } catch (e) {}

  if (!geo) {
    try {
      const res = await fetch('https://api.apogeoapi.com/v1/ip/auto', {
        headers: { 'X-API-Key': APOGEO_KEY }
      });
      if (!res.ok) return;
      const data = await res.json();
      geo = {
        currency: data.country.currency,
        rate: data.country.currencyRate, // USD per 1 unit of local currency
      };
      sessionStorage.setItem(cacheKey, JSON.stringify(geo));
    } catch (e) {
      return; // fail silently — visitor sees shop currency
    }
  }

  if (geo.currency === SHOP_CURRENCY || !geo.rate) return;

  // Convert all prices on the page
  document.querySelectorAll('[data-shopify-price]').forEach((el) => {
    const shopAmount = parseFloat(el.dataset.shopifyPrice);
    if (isNaN(shopAmount)) return;

    // shopAmount is in SHOP_CURRENCY. Convert to USD if needed, then to local.
    let usdAmount = shopAmount;
    if (SHOP_CURRENCY !== 'USD') {
      // For non-USD shops, you need a second lookup or hardcode the SHOP->USD rate.
      // Most stores are USD-base; if yours isn't, fetch /exchange-rates/{SHOP_CURRENCY} once.
      return;
    }

    const localAmount = usdAmount * geo.rate;
    const formatted = new Intl.NumberFormat(undefined, {
      style: 'currency',
      currency: geo.currency,
      maximumFractionDigits: 0,
    }).format(localAmount);

    el.dataset.originalText = el.textContent;
    el.textContent = formatted + ' (≈ ' + el.dataset.originalText.trim() + ')';
  });
})();
</script>

Marking up your prices

In your Liquid templates (e.g. product.liquid, cart.liquid), add the data-shopify-price attribute alongside whatever Liquid-generated price markup you have:

{% raw %}<span data-shopify-price="{{ product.price | money_without_currency }}">
  {{ product.price | money }}
</span>{% endraw %}

The data-shopify-price holds the raw number (no currency symbol). The visible {% raw %}{{ product.price | money }}{% endraw %} is what shows by default if JavaScript is disabled or fails.

Public API key for client-side use

You don't want to put your secret API key in client-side JavaScript — it'd be scrapable. ApogeoAPI supports public keys (prefix pk_live_) that have rate limits per IP and can only call read-only endpoints. Generate one in your dashboard at app.apogeoapi.com.

If your dashboard doesn't yet show a "Public key" option, contact support@apogeoapi.com — public-key support is rolling out and may need to be enabled per account.

Caveats and edge cases

  • Cart and checkout. Shopify's cart and checkout pages will show the canonical shop currency, NOT the converted amount. This is intentional — the actual transaction happens in shop currency. The widget only changes display on browse pages.
  • VPN visitors. Detected currency reflects VPN exit node. A user in Argentina on a US VPN will see USD prices. Acceptable for this use case.
  • Rounding. maximumFractionDigits: 0 rounds to whole units. Helpful for currencies with high inflation (ARS, COP) where decimals look weird. For EUR/GBP/USD, you might prefer 2.
  • Daily cache. The script caches the visitor's geo + rate for the calendar day in sessionStorage. If a visitor opens your site at 23:59 and again at 00:01, they re-fetch — that's fine and rare.

Why this beats Shopify currency apps

SolutionMonthly costTheme lock-inDisplay vs charge
Shopify Markets (Plus only)Plus subscription requiredNoneCharges in local currency (FX spread)
BEST Currency Converter app$5/moManual install per themeDisplay only
Multiple Currency app$10/moTheme injectionDisplay only
This tutorial + ApogeoAPI$0 (free tier)NoneDisplay only

When you'd want Shopify Markets instead

If your store does > $1M/year and you want Shopify to handle multi-currency CHARGING (not just display), Shopify Markets is the right choice — it handles the FX spread, payments, taxes, and compliance per region. For everything else, the script above does 90% of the work for $0.

ApogeoAPI free tier: 1,000 req/month. With sessionStorage caching, that's roughly 1,000 unique visitors per month — enough for most stores under $50K/year. Get a key at apogeoapi.com.

Try ApogeoAPI free

1,000 requests/month forever. 14-day full-access trial. No credit card.

Get your free API key