Show Webflow Ecommerce prices in a shopper's currency with one Webflow Cloud route and a storefront script, while checkout still charges your store currency.
A shopper in Toronto looking at a price in US dollars has to guess what it costs at home, and many leave to check. Webflow Ecommerce charges in one store currency, so the fix is to show the converted price and say plainly that checkout charges your store currency.
In this guide, we’ll do that with a small Next.js route on Webflow Cloud that serves your rates, plus a storefront script that converts tagged prices with the browser's Intl.NumberFormat and remembers each shopper's choice. Your team updates rates in one environment variable without touching the Designer, and the cart and checkout stay exactly as they are.
What do you need to add multi-currency support to Webflow Ecommerce?
Here are the six prerequisites you need:
- Ecommerce plan: A site on one of the Ecommerce plans on the Webflow pricing page (Standard, Plus, or Advanced), with at least one published product to convert.
- Webflow Cloud access: Included from Starter (free) up. The script's same-domain request needs the app mounted on your custom domain, which is not available on Starter or Basic site plans.
- Node.js 22.13.0 or later: The engine floor of the Webflow CLI 2.x that the deployment step installs.
- npm: Webflow Cloud's docs state: "Currently, Webflow Cloud supports only the npm package manager," so pnpm and yarn commands will not carry over.
- Next.js 15+: This is the floor Webflow lists for bringing your own app with a Next.js app on Webflow Cloud.
- Rate owner: A base currency (the one your store charges in), display rates, and a named person who updates them (e.g., weekly), because stale rates fail quietly.
Webflow Cloud requires a custom domain on the app to work with the storefront script. While Next.js is used here, Astro 5+ and Vite 6.1+ are also supported. The setup relies on a shared mount path configured during deployment.
6 steps to add multi-currency pricing to Webflow Ecommerce in Webflow Cloud
The build pairs a small Next.js app on Webflow Cloud that returns rates as JSON with a storefront script that converts tagged prices in the browser. Server work comes first, then the Designer work and the script that connects the two through the shared mount path.
1. Create the Next.js app for Webflow Cloud
Scaffold a TypeScript Next.js project with the App Router and npm as the package manager. Pinning npm at creation matters because Webflow Cloud supports only npm.
Run this from the folder where you keep projects (the version pin keeps the scaffold on Next.js 15):
npx create-next-app@15 currency-rates --typescript --app --use-npm
cd currency-rates
Leave two things out of this project. Do not add export const runtime = 'edge' to any file, because Webflow Cloud deploys Next.js through the OpenNext Cloudflare adapter, which does not support the Next.js edge runtime.
And if you add request middleware later, keep the file named middleware.ts; Next 16's renamed proxy.ts runs on the Node runtime, and Webflow's framework docs state: "Only Edge runtime middleware works on the Workers runtime." You should now have a local project that starts with npm run dev and serves the default page at localhost.
2. Store the base currency and rates as environment variables
Define two variables: one naming the currency your store charges in, and one holding a JSON object of display rates relative to it. Keeping rates in configuration rather than code means an update is a single value change instead of a code change and review.
These are the variables the route reads, plus an optional RATES_UPDATED date that the payload returns so you can show when rates last changed:
| Variable | Example value | Purpose |
|---|---|---|
BASE_CURRENCY |
USD |
The ISO currency code your store charges in; prices on the page are read as this currency |
CURRENCY_RATES |
{"EUR":0.92,"GBP":0.79,"CAD":1.37} |
Display rates per one unit of the base currency; the example values are illustrative, so set your own |
| Variable → Example value → Purpose |
|---|
BASE_CURRENCY |
USD |
| The ISO currency code your store charges in; prices on the page are read as this currency |
CURRENCY_RATES |
{"EUR":0.92,"GBP":0.79,"CAD":1.37} |
| Display rates per one unit of the base currency; the example values are illustrative, so set your own |
For local development, put both in .env.local at the project root. Neither is secret, but if you later swap in a paid rate provider, its API key belongs in a secret variable; Webflow Cloud redacts secrets from build logs, and both secret and non-secret variables are available at build time and at runtime, with a limit of 100 per environment.
After restarting npm run dev, both values are readable from process.env on the server.
3. Write the rates Route Handler
Create a GET Route Handler that validates the two variables and returns them as one JSON payload. Validation makes configuration errors explicit: a malformed CURRENCY_RATES string would otherwise throw inside JSON.parse and return an opaque error page to every shopper.
The route is public and reads only configuration, so it needs no authentication. If you later call a paid rate provider from it, fetch on a schedule and store the result instead of calling the provider per request, so a burst of shoppers cannot run up your bill.
Save this as app/api/rates/route.ts:
// app/api/rates/route.ts
import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
type RatesPayload = {
base: string;
rates: Record<string, number>;
updated: string | null;
};
export async function GET() {
const base = process.env.BASE_CURRENCY;
const raw = process.env.CURRENCY_RATES;
if (!base || !raw) {
return NextResponse.json(
{ error: 'Currency configuration missing' },
{ status: 500 }
);
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return NextResponse.json(
{ error: 'CURRENCY_RATES is not valid JSON' },
{ status: 500 }
);
}
const code = /^[A-Z]{3}$/;
if (
!code.test(base) ||
typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
!Object.entries(parsed).every(([k, v]) => code.test(k) && typeof v === 'number' && v > 0)
) {
return NextResponse.json(
{ error: 'BASE_CURRENCY and every CURRENCY_RATES key must be a 3-letter ISO code with a positive rate' },
{ status: 500 }
);
}
const body: RatesPayload = {
base,
rates: parsed as Record<string, number>,
updated: process.env.RATES_UPDATED ?? null,
};
return NextResponse.json(body);
}
The dynamic = 'force-dynamic' export tells Next.js to read the variables per request instead of freezing them into a static response at build time. The validation rejects anything that is not a three-letter currency code with a positive rate, because the browser's Intl.NumberFormat throws on a malformed code and would stop the script halfway through the page.
Webflow Cloud replaces any Cache-Control header on responses with private, no-cache, so the storefront script keeps its own copy of the rates for an hour instead.
4. Deploy the app to a Webflow Cloud environment
Install the Webflow CLI, connect the project to a Webflow Cloud project on your Ecommerce site, and deploy it to an environment with a mount path such as /currency. The app's routes are served under the mount path, so /api/rates locally becomes /currency/api/rates on your domain.
Use Node.js 22.13.0 or later, the engine floor of the Webflow CLI 2.x, and install it globally:
npm install -g @webflow/webflow-cli@2
Follow the deployment flow in Webflow's developer docs to link the project, create an environment, and set its mount path; this guide uses /currency.
Before the first deploy, add BASE_CURRENCY and CURRENCY_RATES to the environment's variables in Webflow Cloud. Treat .env.local as a local development file, since the deployed app should read both values from the Webflow Cloud environment. You do not need to set a base path in next.config; Webflow injects the base path at build time.
In the environment settings, confirm the mount path and that both variables are set.
Once the deployment finishes, visit https://your-domain.com/currency/api/rates in a browser. You should see the same JSON payload you saw locally, now served from Webflow Cloud.
5. Tag price elements in the Designer
Mark every price you want converted with a custom attribute named data-price-convert, and add one empty element with the attribute data-currency-switcher where the dropdown should appear.
Attributes keep the script independent of class names, so a designer can restyle or rename classes without breaking conversion.
Start on the product template page: select the text element bound to the product price and add a custom attribute named data-price-convert. The script matches on the attribute name alone, so whatever value you give it does not matter.
Repeat on product cards in any Collection list on the shop and home pages. For the switcher, a div in the navbar works well; if your navbar is shared across pages, placing it there puts the switcher on every page that uses that navbar.
Leave cart and checkout totals untagged. Those figures come from the cart and checkout, which this build leaves alone. Set BASE_CURRENCY to the same currency your Webflow Ecommerce store charges in; if they differ, every converted price will be wrong, and no error will appear.
Publish the site, then inspect a product page in your browser's developer tools; each tagged price element should carry the data-price-convert attribute in the rendered HTML.
6. Add the conversion script to the storefront
Paste this script into your site's footer custom code; it fetches the rates and builds the switcher, then rewrites every tagged price. The script reads the page's price elements as soon as it runs, so it must load after them.
The fetch URL hardcodes the mount path because client-side requests are not rewritten for you; Webflow's bring-your-own-app page states that "Client-side fetch calls must manually include the base path," and this script follows the same rule, since a published storefront page gets no rewriting either.
This relative URL only works because the app shares the storefront's domain through the Premium-or-higher custom-domain mount.
Set MOUNT_PATH to your environment's mount path before publishing. Footer custom code needs a paid plan, which every Ecommerce plan includes.
<script>
(function () {
var MOUNT_PATH = '/currency';
var STORAGE_KEY = 'webflow-preferred-currency';
var RATES_KEY = 'webflow-currency-rates';
var RATES_MAX_AGE = 60 * 60 * 1000;
function readStore(store, key) {
try { return store.getItem(key); } catch (e) { return null; }
}
function writeStore(store, key, value) {
try { store.setItem(key, value); } catch (e) {}
}
var priceNodes = document.querySelectorAll('[data-price-convert]');
var switchers = document.querySelectorAll('[data-currency-switcher]');
if (!priceNodes.length && !switchers.length) return;
function readBaseAmount(node) {
if (!node.dataset.baseAmount) {
node.dataset.baseText = node.textContent;
node.dataset.baseAmount = node.textContent.replace(/[^0-9.]/g, '');
}
return parseFloat(node.dataset.baseAmount);
}
function render(currency, data) {
priceNodes.forEach(function (node) {
var amount = readBaseAmount(node);
if (isNaN(amount)) return;
if (currency === data.base) {
node.textContent = node.dataset.baseText;
return;
}
var rate = data.rates[currency];
if (typeof rate !== 'number') return;
node.textContent = new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currency
}).format(amount * rate);
});
}
function buildSwitcher(container, data, current) {
var select = document.createElement('select');
select.setAttribute('aria-label', 'Display currency');
var codes = [data.base].concat(
Object.keys(data.rates).filter(function (code) {
return code !== data.base;
})
);
codes.forEach(function (code) {
var option = document.createElement('option');
option.value = code;
option.textContent = code;
if (code === current) option.selected = true;
select.appendChild(option);
});
select.addEventListener('change', function () {
writeStore(localStorage, STORAGE_KEY, select.value);
document.querySelectorAll('[data-currency-switcher] select').forEach(function (other) {
other.value = select.value;
});
render(select.value, data);
});
container.appendChild(select);
var note = document.createElement('small');
note.textContent = 'Estimates only. Checkout charges ' + data.base + '.';
container.appendChild(note);
}
function loadRates() {
var cached = readStore(sessionStorage, RATES_KEY);
if (cached) {
try {
var entry = JSON.parse(cached);
if (Date.now() - entry.savedAt < RATES_MAX_AGE) return Promise.resolve(entry.data);
} catch (e) {}
}
return fetch(MOUNT_PATH + '/api/rates')
.then(function (res) {
if (!res.ok) throw new Error('Rates request failed with status ' + res.status);
return res.json();
})
.then(function (data) {
writeStore(sessionStorage, RATES_KEY, JSON.stringify({ savedAt: Date.now(), data: data }));
return data;
});
}
loadRates()
.then(function (data) {
var saved = readStore(localStorage, STORAGE_KEY);
var current = saved && (saved === data.base || data.rates[saved]) ? saved : data.base;
switchers.forEach(function (container) {
buildSwitcher(container, data, current);
});
render(current, data);
})
.catch(function (err) {
console.warn(err);
});
})();
</script>
The script caches each element's original text in a data attribute on first read, so switching back to the base currency restores your exact store formatting instead of a reformatted guess. If the request fails, prices stay as published.
After publishing, pick a currency from the dropdown on a product page: tagged prices should change immediately, and a reload should keep the shopper's choice.
What causes Webflow Ecommerce currency conversion to fail?
Most failures come from the build or a missing mount path; the rest come from unset environment variables or prices parsed as the wrong number.
Webflow Cloud deploy fails with a runtime error on the rates route
Cause: The route file contains export const runtime = 'edge', usually copied in good faith from Webflow's own bring-your-own-app page, which still tells readers to add that directive to API routes. The confusion comes from "edge" meaning two things.
Webflow Cloud runs on Cloudflare Workers, an edge platform, and the docs describe that as deploying "using the Edge runtime." The Next.js edge runtime target is a separate setting, and the OpenNext Cloudflare adapter that Webflow Cloud uses to deploy Next.js does not support it.
Fix: Delete the runtime export from every route and page, then redeploy. Your route already runs on the Workers edge platform without it. Keep export const dynamic = 'force-dynamic', which only controls caching.
The switcher never appears, and the console shows a 404 for /api/rates
Cause: The storefront script is requesting /api/rates at the domain root instead of under the mount path. Webflow injects the base path into the Next.js app at build time, so routes respond at /currency/api/rates, but a fetch made from a published Webflow page is not rewritten. The request most likely reaches your Webflow site instead of the app, which is why the console shows a 404.
Fix: Set MOUNT_PATH in the script to the exact mount path of your Webflow Cloud environment, including the leading slash and without a trailing one. Confirm the full URL loads JSON in a browser tab before republishing.
If you run separate staging and production environments with different mount paths, keep one script per environment so a staging path never ships to production.
The rates URL returns "Currency configuration missing" in production but works locally
Cause: The variables exist in .env.local on your machine but were never added to the Webflow Cloud environment. The deployed app reads the environment's variables in Webflow Cloud, so if they were never set there, process.env.BASE_CURRENCY is undefined and the route's missing-configuration guard returns a 500.
A related version shows "CURRENCY_RATES is not valid JSON," which usually means smart quotes or a trailing comma crept in while pasting.
Fix: Add both variables to the environment in Webflow Cloud, matching the names exactly, and paste the JSON from a plain text editor. Webflow Cloud makes variables available at build time and at runtime; I redeploy after changing them anyway, so the build and the running app read the same values.
Converted prices are off by a factor of 100 or 1,000
Cause: The script strips everything except digits and periods, which assumes your store displays prices like $1,299.00. A store formatted with a comma as the decimal separator (1.299,00 €) loses its decimal meaning, so the parsed amount is wrong before any rate is applied. Tagging an element that holds extra numbers, such as "2 for $40", causes the same distortion.
Fix: Tag only the element that contains the bare price, never a wrapper with quantities or labels. If your store uses comma decimals, change the parser in readBaseAmount to remove periods first and then replace the comma with a period before calling parseFloat.
Test one product of every price shape in your catalog (whole amounts and four-digit prices with cents) before trusting the switcher.
If your product template updates the price text when a shopper selects a variant, the cached data-base-amount no longer matches the new price. Watch each tagged price with a MutationObserver.
When its text changes to a value that isn't the script's own output, clear data-base-amount and data-base-text, then call render again with the saved currency so the new variant price becomes the base. Test by switching currency, then switching variants on a product whose variants have different prices.
What you can build next with Webflow Cloud and Webflow Ecommerce
I'd build scheduled rate updates next, pulling rates from a provider on a schedule instead of editing CURRENCY_RATES by hand, with the provider's key stored as a secret variable.
For stores that want shoppers to pay in another currency, a separate Stripe checkout for specific products is one way to charge in that currency.
For the request and CPU limits the rates route runs within, Webflow's developer docs list every Webflow Cloud limit.
Frequently asked questions
Will shoppers be charged in the currency they select?
No. This build only rewrites text on tagged elements and never touches the cart or payment flow. Webflow Ecommerce charges in the single store currency set in your Ecommerce settings, and the shopper's card issuer converts that charge at its own rate. The script adds a short note under the switcher saying prices are estimates and checkout charges your store currency, so the checkout total doesn't surprise anyone.
Can I use Webflow Localize alongside the currency switcher?
Partly. Webflow's help center says Localize doesn't support Ecommerce Site plan features, so products, categories and product pages can't be localized on a store. The switcher still works on every page because it only reads tagged price elements and the rates route, and it saves each shopper's choice in localStorage.
What happens if BASE_CURRENCY doesn't match my store currency?
The script treats every published price as BASE_CURRENCY, so a mismatch multiplies each price by the wrong rate, and nothing reports an error. If your store charges in euros but BASE_CURRENCY says USD, every conversion is off. Set it to the currency your store charges in, and keep every entry in CURRENCY_RATES relative to it.
Does the rates route count against my Webflow Cloud usage limits?
Yes. Every rates request counts toward your Webflow Cloud request allowance, but the script keeps the rates in sessionStorage for an hour, so a shopper browsing many products triggers one request rather than one per page.
Can I add Node packages to fetch live rates?
Yes, if your package runs on Workers. Cloudflare only enables node:fs by default from compatibility date 2025-09-01, later than the date in Webflow Cloud's example configuration, so check whether a rate library reads files before you add it. A plain fetch to the provider's API is the safest choice.





