Shopify Currency Widget with Live Exchange Rates (No App Required)
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
- JavaScript runs on every page load.
- Fetches the visitor's country (cached at edge).
- Fetches the live USD->X exchange rate.
- Finds all elements marked with
data-shopify-priceand overlays the converted amount. - 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: 0rounds to whole units. Helpful for currencies with high inflation (ARS, COP) where decimals look weird. For EUR/GBP/USD, you might prefer2. - 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
| Solution | Monthly cost | Theme lock-in | Display vs charge |
|---|---|---|---|
| Shopify Markets (Plus only) | Plus subscription required | None | Charges in local currency (FX spread) |
| BEST Currency Converter app | $5/mo | Manual install per theme | Display only |
| Multiple Currency app | $10/mo | Theme injection | Display only |
| This tutorial + ApogeoAPI | $0 (free tier) | None | Display 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