Shopify Exchange Rates: Local Prices, Converter, Market Sync
Three ways to use a currency API with Shopify: show an approximate local price on product pages, embed a currency converter with no key, or sync fixed market prices nightly through the Admin API. Theme, Worker and Node code included.
Option 1: Embed a currency converter (no key, no code)
Paste this into a Custom Liquid block on your product template. It shows a
converter pre-filled with the product's price, using the daily ECB reference rates (30+ major
currencies) with no key. Don't add data-api-key in a theme, because theme code is
public. For live rates in 160+ currencies, use Option 2. See the
widget reference.
<div class="art-currency-widget"
data-base="{{ cart.currency.iso_code }}"
data-target="EUR"
data-amount="{{ product.selected_or_first_available_variant.price | divided_by: 100.0 }}"></div>
<script async src="https://allratestoday.com/widget.js"></script> Option 2: Show an estimated price in the visitor's currency
Detect the visitor's currency with the keyless IP-to-currency endpoint, then fetch the rate through a small proxy. A theme's JavaScript is visible to anyone, so never put your API key in the theme.
Step 1: Deploy a rate proxy
Create a Cloudflare Worker with this code. Add your API key as the secret ALLRATES_KEY
and your store's URL as STORE_ORIGIN. Each Cloudflare location fetches a currency pair
at most once every 15 minutes, however much traffic you get, so a busy store still uses only a
few thousand calls a month.
// Cloudflare Worker (free plan is enough): holds your key, caches each rate 15 min.
export default {
async fetch(req, env, ctx) {
const url = new URL(req.url);
const from = (url.searchParams.get('from') || '').toUpperCase();
const to = (url.searchParams.get('to') || '').toUpperCase();
if (!/^[A-Z]{3}$/.test(from) || !/^[A-Z]{3}$/.test(to)) {
return new Response('{"error":"from and to must be ISO 4217 codes"}', { status: 400 });
}
const cacheKey = new Request(`https://rates.internal/${from}-${to}`);
let res = await caches.default.match(cacheKey);
if (!res) {
const r = await fetch(
`https://allratestoday.com/api/v1/rates?source=${from}&target=${to}`,
{ headers: { Authorization: `Bearer ${env.ALLRATES_KEY}` } }
);
if (!r.ok) return new Response('{"error":"rate unavailable"}', { status: 502 });
const [{ rate }] = await r.json();
res = new Response(JSON.stringify({ from, to, rate }), {
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=900' },
});
ctx.waitUntil(caches.default.put(cacheKey, res.clone()));
}
const out = new Response(res.body, res);
out.headers.set('Access-Control-Allow-Origin', env.STORE_ORIGIN); // e.g. https://yourstore.com
return out;
},
}; Step 2: Add the snippet to your product template
In Online Store → Themes → Edit code, add this under the price in your product section (or in a Custom Liquid block):
{%- comment -%} sections/main-product.liquid, under the price {%- endcomment -%}
<p class="approx-local-price" hidden
data-amount="{{ product.selected_or_first_available_variant.price | divided_by: 100.0 }}"
data-currency="{{ cart.currency.iso_code }}"></p>
<script>
(async () => {
const el = document.querySelector('.approx-local-price');
if (!el) return;
try {
// 1. Visitor's currency from their IP (keyless, CORS-open)
const { currency } = await fetch('https://allratestoday.com/api/open/geo').then(r => r.json());
if (!currency || currency === el.dataset.currency) return;
// 2. Rate through your Worker, so the key stays private
const { rate } = await fetch(
'https://rates.yourname.workers.dev/?from=' + el.dataset.currency + '&to=' + currency
).then(r => r.json());
const local = new Intl.NumberFormat(undefined, { style: 'currency', currency })
.format(el.dataset.amount * rate);
el.textContent = 'About ' + local + ' (estimate, you pay in ' + el.dataset.currency + ')';
el.hidden = false;
} catch (e) { /* leave the shop price on its own */ }
})();
</script> product.price is in cents in the cart's currency, which is why the snippet divides
by 100 and converts from cart.currency.iso_code. Label the result as an estimate if
you still charge in your own currency.
Option 3: Sync fixed market prices every night
If you sell in several currencies, Markets can use fixed prices from a price list instead of automatic conversion. A nightly job can recompute those prices from the day's mid-market rate and apply your own rounding, so your EUR prices follow the dollar without converting at checkout. You need an Admin API access token from a custom app and the ID of the market's price list. Fixed-price availability depends on your Shopify plan, so check Shopify's Markets documentation.
// sync-market-prices.mjs: run nightly (cron, GitHub Actions, a Worker cron).
// Sets fixed EUR prices on a market's price list from today's USD->EUR rate.
const SHOP = 'yourstore.myshopify.com';
const PRICE_LIST_ID = 'gid://shopify/PriceList/123456789';
const VARIANTS = [
{ id: 'gid://shopify/ProductVariant/111', usd: 49.0 },
{ id: 'gid://shopify/ProductVariant/222', usd: 129.0 },
];
const [{ rate }] = await fetch(
'https://allratestoday.com/api/v1/rates?source=USD&target=EUR',
{ headers: { Authorization: `Bearer ${process.env.ALLRATES_KEY}` } }
).then(r => r.json());
// Charm pricing: round up to the next whole euro, minus one cent.
const charm = (x) => (Math.ceil(x) - 0.01).toFixed(2);
const prices = VARIANTS.map(v => ({
variantId: v.id,
price: { amount: charm(v.usd * rate), currencyCode: 'EUR' },
}));
const res = await fetch(`https://${SHOP}/admin/api/2025-10/graphql.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_TOKEN,
},
body: JSON.stringify({
query: `mutation($id: ID!, $prices: [PriceListPriceInput!]!) {
priceListFixedPricesAdd(priceListId: $id, prices: $prices) {
prices { variant { id } price { amount currencyCode } }
userErrors { field message }
}
}`,
variables: { id: PRICE_LIST_ID, prices },
}),
}).then(r => r.json());
console.log(`USD->EUR ${rate}`, JSON.stringify(res.data.priceListFixedPricesAdd));
One run costs a single rate call. For many currencies, request them together
(target=EUR,GBP,CAD) and loop over the rows.
Which plan do I need?
| Option | Key | Typical usage |
|---|---|---|
| Converter widget | None (ECB rates) | Unmetered |
| Estimated local price | Paid plan (commercial use), from €4.99/mo | Up to about 2,900 calls/mo per currency pair per Cloudflare location (15-minute cache) |
| Nightly price sync | Paid plan, from €4.99/mo | About 30 calls/mo |
A store is commercial use, which starts on the €4.99 plan. Build and test on the free key first. Plans and limits are on the pricing page.
FAQ
Will this change what customers pay?
Options 1 and 2 only display information, so checkout is unchanged. Option 3 changes the fixed prices in a market's price list, which customers in that market then pay.
Can I put the API key in my theme?
No. Theme code is public, so anyone could copy the key and use your quota. Keep it in a proxy, as in Option 2.
Which rate does Shopify use for automatic conversion?
Shopify's own rates, plus its conversion fee where it applies. This guide doesn't change that. It adds information or sets fixed prices that replace automatic conversion for a market.