Localize Stripe Prices to the Visitor's Currency in 25 Lines
Source: Dev.to
Overview
Stripe’s Price object is tied to a single currency (e.g., $79 USD).
For conversion‑rate optimization you can display the price in the visitor’s local currency (ARS, BRL, EUR, …) while still charging the canonical USD amount on the backend.
How it works
- Store the canonical price in USD – never modify the Stripe Price.
- Detect the visitor’s country on the server (IP‑based).
- Map the country to its local currency.
- Fetch the live USD → local‑currency rate from an FX API.
- Render the localized amount (e.g., “≈ AR$ 7 900”) next to the USD price.
- When the user clicks Buy, Stripe processes the payment in USD; the conversion is display‑only.
This approach gives visitors familiar numbers while keeping Stripe’s accounting simple and chargeback‑safe.
Implementation
Helper: localizedPrice
// lib/localized-price.ts
import { headers } from 'next/headers';
interface LocalizedPrice {
usd: number;
display: string;
currency: string;
rate: number;
}
const APOGEO = 'https://api.apogeoapi.com/v1';
export async function localizedPrice(usdAmount: number): Promise {
const ip =
headers().get('x-forwarded-for')?.split(',')[0] ??
headers().get('x-real-ip') ??
'';
if (!ip) return fallback(usdAmount, 'USD', 1);
// 1️⃣ Detect visitor country
const ipRes = await fetch(`${APOGEO}/ip/${ip}`, {
headers: { 'X-API-Key': process.env.APOGEOAPI_KEY! },
next: { revalidate: 3600 }, // 1 h
});
if (!ipRes.ok) return fallback(usdAmount, 'USD', 1);
const { country } = await ipRes.json();
const currency: string = country.currency;
if (currency === 'USD') return fallback(usdAmount, 'USD', 1);
// 2️⃣ Fetch live FX rate
const fxRes = await fetch(`${APOGEO}/exchange-rates/${currency}`, {
headers: { 'X-API-Key': process.env.APOGEOAPI_KEY! },
next: { revalidate: 14400 }, // 4 h
});
if (!fxRes.ok) return fallback(usdAmount, 'USD', 1);
const { usdRate }: { usdRate: number } = await fxRes.json();
const localAmount = Math.round(usdAmount * usdRate);
const display = new Intl.NumberFormat(undefined, {
style: 'currency',
currency,
maximumFractionDigits: 0,
}).format(localAmount);
return { usd: usdAmount, display, currency, rate: usdRate };
}
function fallback(usdAmount: number, currency: string, rate: number): LocalizedPrice {
return {
usd: usdAmount,
display: `$${usdAmount} ${currency}`,
currency,
rate,
};
}
Using the helper in a component
// components/PricingCard.tsx
import { localizedPrice } from '@/lib/localized-price';
export default async function PricingCard() {
const price = await localizedPrice(79); // Stripe price in USD
return (
<>
<h2>Professional Plan</h2>
<p>$79 USD</p>
{price.currency !== 'USD' && (
<p>≈ {price.display} at today's rate</p>
)}
<button>Buy now</button>
</>
);
}
Practical Tips
- Never change the Stripe Price object per currency; keep a single USD price and use display‑only localization.
- Round appropriately – e.g., ARS has no useful cents, so
maximumFractionDigits: 0removes fractions. - Graceful fallback – if the FX API is unavailable, show the USD amount instead of “$undefined”.
- Cache rates for 4 hours (matching the API’s refresh cadence) to avoid excess requests.
- Adaptive Pricing (Stripe‑provided per‑currency Prices) incurs a ~2 % FX spread that Stripe keeps and hides the visitor’s original display price. The display‑only method is free, transparent, and simpler.
Optional: Currency Switcher
If you want users to override auto‑detection:
- Store the chosen currency in a cookie.
- Pass that value to
localizedPrice(add an optionaloverrideCurrencyparameter). - The same helper will return the appropriate display amount.
Cost & Limits
- ApogeoAPI free tier: 1 000 requests/month.
- With a 4‑hour cache, this covers roughly 5 000 page views per month.
- Get a key at .