Show localized prices on your pricing page or checkout
Você define os preços em uma moeda-base (ex.: USD) e mostra ao visitante
o valor aproximado na moeda local. Busque as taxas uma vez por sessão,
guarde em localStorage com um TTL, formate com Intl.NumberFormat
e tenha um fallback para o preço-base se a busca falhar.
You keep prices in one base currency (e.g. USD) and show the visitor the
approximate amount in their local currency. Fetch the rates once per session,
cache them in localStorage with a TTL, format with Intl.NumberFormat,
and fall back to the base price if the fetch fails.
Base: https://moneta.api.insyde.one — aberta, sem autenticação / open, no auth.
Uma única chamada traz todas as moedas que você precisa. / A single call brings every currency you need.
GET /rates?base=USD"es=BRL,EUR,GBP,JPY
[
{ "date": "2026-06-03", "base": "USD", "quote": "BRL", "rate": 5.0203 },
{ "date": "2026-06-03", "base": "USD", "quote": "EUR", "rate": 0.9210 }
]
As taxas mudam no máximo uma vez por dia útil, então um TTL de algumas horas é seguro.
Guardamos o mapa quote → rate e o carimbo de tempo.
Rates change at most once per business day, so a TTL of a few hours is safe. We store the
quote → rate map plus a timestamp.
const BASE = "USD";
const QUOTES = ["BRL", "EUR", "GBP", "JPY"];
const TTL = 6 * 60 * 60 * 1000; // 6h em ms
const KEY = "moneta-rates-v1";
async function getRates() {
const cached = JSON.parse(localStorage.getItem(KEY) || "null");
if (cached && Date.now() - cached.ts < TTL) return cached.rates;
const url = `https://moneta.api.insyde.one/rates?base=${BASE}"es=${QUOTES.join(",")}`;
const res = await fetch(url);
if (!res.ok) throw new Error("rates unavailable");
const rows = await res.json();
const rates = Object.fromEntries(rows.map(r => [r.quote, r.rate]));
rates[BASE] = 1; // base → base
localStorage.setItem(KEY, JSON.stringify({ ts: Date.now(), rates }));
return rates;
}
Intl.NumberFormat cuida do símbolo, dos separadores e das casas decimais
corretas para cada moeda — não monte isso à mão.
Intl.NumberFormat handles the symbol, separators and correct decimal places
for each currency — do not hand-roll it.
function format(amount, currency, locale) {
return new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(amount);
}
O preço-base é a fonte da verdade. Se a busca de taxas falhar, mostramos o preço-base
sem prefixo de estimativa. Se der certo, mostramos o valor convertido com ≈.
The base price is the source of truth. If the rate fetch fails, we show the base price with
no estimate prefix. If it succeeds, we show the converted value with ≈.
const BASE_PRICE = 29; // 29 USD
async function renderPrice(el, currency, locale) {
const baseLabel = format(BASE_PRICE, BASE, "en-US");
try {
const rates = await getRates();
const rate = rates[currency];
if (!rate) throw new Error("no rate");
el.textContent = `≈ ${format(BASE_PRICE * rate, currency, locale)}`;
el.title = `Charged as ${baseLabel}`;
} catch {
el.textContent = baseLabel; // fallback: preço-base
}
}
renderPrice(document.getElementById("price"), "BRL", "pt-BR");
Resultado típico / Typical output: ≈ R$ 145,59 com a dica "Charged as $29.00".
Use o locale do navegador como pista, mas deixe o usuário trocar manualmente.
Nunca presuma a moeda só pelo idioma.
Use the browser locale as a hint, but let the user switch manually. Never assume
currency from language alone.
const locale = navigator.language || "en-US"; // ex.: "pt-BR"
// mapeie locale → moeda com sua própria tabela, ou ofereça um seletor
≈),
cobre na moeda-base e deixe o seu PSP aplicar a taxa real na liquidação.
≈), charge in the base
currency, and let your PSP apply the real rate at settlement.
localStorage evita chamadas repetidas sem servir dados velhos.
localStorage TTL avoids repeat calls without serving stale data.