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.

Before you start: if you use Shopify Payments with Shopify Markets, Shopify already converts checkout prices at its own rates, and you can set a manual rate per market in Settings → Markets. Use this guide when you want something Markets doesn't do: estimated local prices on a store that sells in one currency, a converter on product pages, or fixed market prices set from a rate and rounding rule you control.

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?

OptionKeyTypical usage
Converter widgetNone (ECB rates)Unmetered
Estimated local pricePaid plan (commercial use), from €4.99/moUp to about 2,900 calls/mo per currency pair per Cloudflare location (15-minute cache)
Nightly price syncPaid plan, from €4.99/moAbout 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.

Add live rates to your store

Free key in 30 seconds, no credit card.

Get free API key Read the docs

Related