Voltar ao Blog

Regras de arredondamento de moedas para desenvolvedores: casas decimais, unidades menores e conversão segura

V
Vlado Grigirov
August 21, 2026
Currency API Exchange Rates Currency Rounding Minor Units ISO 4217 Finexly Developer Guide

Um cliente em Tóquio compra uma assinatura de US$ 19,99. Seu código multiplica pela taxa USD/JPY, obtém 2942,82785, grava no banco de dados e envia ao seu processador de pagamentos. O processador rejeita — ou pior, aceita e cobra 100 vezes mais. O iene não tem casas decimais, e seu código nunca perguntou.

Arredondamento de moedas é um daqueles problemas que parecem triviais até chegarem à produção. Não é uma questão de formatação; é uma questão de correção. Cada moeda tem seu próprio número de casas decimais, a matemática de ponto flutuante corrompe dinheiro silenciosamente, e no momento em que você converte entre moedas introduz uma decisão de arredondamento que precisa ser tomada de forma deliberada. Este guia cobre as regras que realmente importam: quantas casas decimais cada moeda tem, por que armazenar valores como inteiros, qual modo de arredondamento escolher e como arredondar uma conversão cambial para que seu razão continue fechando no fim do mês.

Unidades menores: quantas casas decimais tem cada moeda?

A unidade menor de uma moeda é sua menor subdivisão transacionável. Para o dólar americano é o centavo, então USD tem duas casas decimais e US$ 19,99 são 1999 centavos. Essa é a representação que os processadores de pagamento esperam, e é a que seu banco de dados deve usar.

A ISO 4217 — o mesmo padrão que fornece códigos de três letras como USD e JPY — também atribui a cada moeda um expoente de unidade menor. A maioria dos desenvolvedores supõe que esse expoente é sempre 2. Não é, e essa suposição é o bug mais caro desta categoria.

Moedas sem casas decimais

Essas moedas não têm subunidade em circulação, então o valor que você envia é o número inteiro de unidades:

  • JPY — iene japonês
  • KRW — won sul-coreano
  • VND — dong vietnamita
  • CLP — peso chileno
  • ISK — coroa islandesa
  • XAF / XOF / XPF — francos CFA e CFP
  • UGX — xelim ugandense
  • PYG — guarani paraguaio
  • RWF, GNF, KMF, DJF, VUV — e várias outras moedas de pequena denominação

Se você tratar JPY como moeda de duas casas decimais e multiplicar por 100 antes de enviar ao processador, acabou de cobrar do seu cliente 100× o valor pretendido.

Moedas com três casas decimais

Sete moedas se subdividem em milésimos em vez de centésimos:

  • KWD — dinar kuwaitiano (1000 fils)
  • BHD — dinar bareinita (1000 fils)
  • OMR — rial omanense (1000 baisa)
  • JOD — dinar jordaniano (1000 fils)
  • TND — dinar tunisiano (1000 milésimos)
  • IQD — dinar iraquiano (1000 fils)
  • LYD — dinar líbio (1000 dirhams)

Aqui a falha vai na direção oposta: trate KWD como duas casas decimais e você cobra um décimo do que pretendia. Uma fatura de KWD 12,500 vira KWD 1,250.

Há até entradas de quatro casas decimais na ISO 4217 — a unidad de fomento chilena (CLF) e a unidad previsional uruguaia (UYW). São unidades contábeis indexadas, não dinheiro em espécie, mas se seu sistema aceita códigos ISO arbitrários, precisa sobreviver a elas.

Quando o padrão e seu processador discordam

Essa é a armadilha que pega equipes que fizeram todo o resto certo. Provedores de pagamento às vezes se desviam da ISO 4217 por razões operacionais. A Adyen, por exemplo, documenta que CLP, CVE, IDR e ISK usam em sua API um número de casas decimais diferente do que o padrão especifica — a ISK tem zero casas sob a ISO 4217, mas precisa ser enviada com duas casas para a Adyen.

A regra: sua tabela de arredondamento é uma propriedade do sistema com o qual você conversa, não uma constante universal. Mantenha uma tabela por integração, popule-a a partir da ISO 4217 e sobrescreva por provedor onde a documentação deles mandar. Nunca deixe 100 fixo no código.

Nunca armazene dinheiro como float

Antes de qualquer discussão sobre arredondamento, a base. O ponto flutuante binário não consegue representar exatamente a maioria das frações decimais:

0.1 + 0.2              // 0.30000000000000004
1.005 * 100            // 100.49999999999999
19.99 * 147.2150       // 2942.8278499999997

Esses dígitos finais não são cosméticos. Passe-os por Math.round() no momento errado e você obtém um valor errado por uma unidade menor, o que basta para reprovar a conciliação.

Duas regras cobrem quase todos os casos:

  1. Armazene valores como inteiros em unidades menores. Uma coluna amount_minor BIGINT mais uma coluna currency CHAR(3). { amount_minor: 1999, currency: "USD" } é inequívoco e corresponde ao que Stripe, Adyen e a maioria dos processadores já esperam.
  2. Faça a aritmética com inteiros ou com um tipo decimal. decimal.Decimal do Python, BigDecimal do Java, NUMERIC do PostgreSQL, ou uma biblioteca monetária JavaScript que encapsule aritmética inteira. Reserve floats para a própria taxa de câmbio, e ainda assim apenas até o ponto da multiplicação.

Se você está desenhando essa camada do zero, nosso guia de design de razão multimoeda aprofunda as decisões de esquema.

O pipeline de conversão: unidades menores entram, unidades menores saem

A conversão de moedas tem exatamente quatro passos, e o arredondamento pertence ao passo três — uma única vez, no fim.

  1. Converta o valor de origem de unidades menores para um valor decimal.
  2. Multiplique pela taxa de câmbio em precisão total.
  3. Arredonde para o expoente de unidade menor da moeda de destino.
  4. Converta de volta para unidades menores inteiras.

Aqui está em JavaScript, com a tabela por moeda fazendo o trabalho:

// Minor unit exponents. Seed from ISO 4217, override per payment provider.
const MINOR_UNITS = {
  USD: 2, EUR: 2, GBP: 2, CHF: 2, CAD: 2, AUD: 2, CNY: 2, INR: 2,
  JPY: 0, KRW: 0, VND: 0, CLP: 0, ISK: 0, XAF: 0, XOF: 0, XPF: 0,
  KWD: 3, BHD: 3, OMR: 3, JOD: 3, TND: 3, IQD: 3, LYD: 3,
};

function exponentFor(currency) {
  const e = MINOR_UNITS[currency];
  if (e === undefined) throw new Error(`Unknown minor unit for ${currency}`);
  return e;
}

/**
 * Convert an integer minor-unit amount from one currency to another.
 * Returns an integer in the target currency's minor units.
 */
function convertMinor(amountMinor, from, to, rate) {
  const fromExp = exponentFor(from);
  const toExp = exponentFor(to);

  const decimalAmount = amountMinor / 10 ** fromExp;   // 1999 -> 19.99
  const converted = decimalAmount * rate;              // full precision, no rounding yet
  return Math.round(converted * 10 ** toExp);          // single rounding step
}

convertMinor(1999, "USD", "JPY", 147.2150);   // 2943      (¥2,943)
convertMinor(1999, "USD", "KWD", 0.30590);    // 6115      (KWD 6.115)
convertMinor(1999, "USD", "EUR", 0.9241);     // 1847      (€18.47)

Repare no que a função não faz: nunca arredonda a taxa, nunca arredonda um valor intermediário e nunca supõe duas casas decimais. Math.round aqui é half-up para positivos — adequado para um checkout, mas leia a próxima seção antes de usá-lo em algo regulado.

Escolhendo um modo de arredondamento

"Arredondar para duas casas" não é uma especificação. Há pelo menos cinco formas defensáveis de desempatar, e sistemas financeiros se importam com qual você escolhe.

Modo2,5 →3,5 →−2,5 →Uso típico
Half up34−3Preços ao consumidor, totais de checkout
Half even (do banqueiro)24−2Contabilidade, juros, impostos, relatórios
Half down23−2Raro; ocasionalmente em código financeiro legado
Ceiling (para cima)34−2Taxas que você nunca pode cobrar a menos
Floor (para baixo / truncar)23−3Repasses que você nunca pode pagar a mais
Half up é o que a maioria entende por "arredondar" e o que Math.round() faz com números positivos. É intuitivo e apropriado para um preço que o cliente está prestes a ver.

Half even, também chamado de arredondamento do banqueiro, manda metades exatas para o dígito par mais próximo. Ao longo de muitas transações ele cancela o viés sistemático de alta introduzido pelo half-up, e por isso é o padrão em sistemas contábeis, no módulo decimal do Python e no próprio IEEE 754. Se você agrega milhares de valores convertidos em um relatório de receita, o half-up inflará o total silenciosamente; o half-even não.

Ceiling e floor existem para riscos assimétricos. Um marketplace que repassa a vendedores pode aplicar floor a cada repasse para nunca distribuir mais do que possui; a diferença vai para uma conta de arredondamento.

O Python torna a escolha explícita, o que é a ergonomia correta:

from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP

MINOR_UNITS = {"USD": 2, "EUR": 2, "JPY": 0, "KWD": 3}

def convert_minor(amount_minor: int, src: str, dst: str,
                  rate: str, mode=ROUND_HALF_EVEN) -> int:
    """Convert integer minor units to integer minor units, exactly once."""
    src_exp, dst_exp = MINOR_UNITS[src], MINOR_UNITS[dst]

    amount = Decimal(amount_minor) / (Decimal(10) ** src_exp)
    converted = amount * Decimal(rate)          # rate passed as a string, not a float

    quantum = Decimal(1).scaleb(-dst_exp)       # 0.01, 1, or 0.001
    rounded = converted.quantize(quantum, rounding=mode)
    return int(rounded.scaleb(dst_exp))

convert_minor(1999, "USD", "JPY", "147.2150")   # 2943
convert_minor(1999, "USD", "KWD", "0.30590")    # 6115

Passar a taxa como string para Decimal importa. Decimal(0.9241) herda o erro do float; Decimal("0.9241") não.

Três bugs de arredondamento que custam dinheiro de verdade

1. Arredondar a taxa antes de multiplicar

Taxas de câmbio costumam carregar de quatro a seis decimais significativos, e truncá-las não é inofensivo. Considere USD/JPY a 147,2150 e uma transferência de US$ 10.000:

  • Taxa completa: 10000 × 147.2150 = ¥1.472.150
  • Taxa arredondada para duas casas (147,21): 10000 × 147.21 = ¥1.472.100

Uma discrepância de ¥50 em uma única transação, puramente por formatar a taxa antes de usá-la. Armazene a taxa na precisão que seu provedor retorna, arredonde apenas o valor resultante e persista a taxa exata usada junto da transação para auditoria. Nosso guia sobre de onde as APIs de câmbio tiram seus dados explica por que essa precisão é significativa em primeiro lugar.

2. Arredondar duas vezes numa conversão com várias pernas

Se você roteia USD → EUR → JPY e arredonda na etapa EUR, jogou fora precisão que a segunda multiplicação depois amplifica. Convertendo US$ 12,34 com USD/EUR a 0,9241 e EUR/JPY a 159,3063:

  • Direto: 12.34 × 147.2150 = 1816.63¥1.817
  • Via perna EUR arredondada: 12.34 × 0.9241 = 11.4034 → arredondado para € 11,40 → 11.40 × 159.3063 = 1816.09¥1.816

Um iene, por um passo de arredondamento desnecessário. Numa rodada de repasses de cinquenta mil transações, isso é um chamado de conciliação. Sempre que houver um par direto disponível, use-o; quando precisar triangular, mantenha o intermediário em precisão total. Veja taxas cruzadas explicadas para a mecânica.

3. Itens de linha que não somam o total

Arredonde cada linha de uma fatura de forma independente e as partes nem sempre somarão o total arredondado. O caso clássico é uma divisão:

$10.00 split three ways
  10.00 / 3 = 3.3333...
  → 3.33 + 3.33 + 3.33 = 9.99   ✗ one cent missing

A correção é alocação, não arredondamento. Arredonde o total uma vez e depois distribua entre as partes, repassando o resto uma unidade menor por vez:

/**
 * Split an integer minor-unit total into `n` parts whose sum is exactly the total.
 * Remainder units are distributed to the earliest parts (largest-remainder method).
 */
function allocate(totalMinor, ratios) {
  const sum = ratios.reduce((a, b) => a + b, 0);
  const shares = ratios.map(r => Math.floor((totalMinor * r) / sum));
  let remainder = totalMinor - shares.reduce((a, b) => a + b, 0);

  for (let i = 0; remainder > 0; i = (i + 1) % shares.length, remainder--) {
    shares[i] += 1;
  }
  return shares;
}

allocate(1000, [1, 1, 1]);   // [334, 333, 333]        → sums to exactly 1000
allocate(9247, [3, 2, 1]);   // [4624, 3082, 1541]     → sums to exactly 9247

Aplique o mesmo padrão após uma conversão cambial: converta e arredonde o total da fatura, depois aloque esse total entre as linhas. As linhas sempre fecharão, porque foram derivadas do total em vez de calculadas isoladamente. Isso importa mais em faturamento multimoeda e em cobrança SaaS com proporcionalização, onde um desvio de um centavo aparece num PDF que o cliente vê.

Arredondamento de dinheiro em espécie é outra regra

A unidade menor de uma moeda diz o menor valor que pode ser registrado. Nem sempre diz o menor valor que pode ser pago em espécie. Vários países retiraram suas menores moedas e arredondam pagamentos em dinheiro no caixa:

  • Suíça — dinheiro arredonda para os 0,05 CHF mais próximos
  • Canadá — a moeda de um centavo foi retirada em 2013; dinheiro arredonda para os 5 centavos mais próximos
  • Suécia — dinheiro arredonda para a coroa inteira mais próxima
  • Países Baixos — dinheiro arredonda para os 5 cêntimos mais próximos

Crucialmente, isso se aplica ao pagamento em espécie, não à fatura. Uma fatura suíça de CHF 12,32 continua registrada como 12,32; só a liquidação em dinheiro arredonda para 12,30, e a diferença de 0,02 é lançada como ajuste de arredondamento. Se você desenvolve software de ponto de venda, modele o arredondamento de espécie como um passo separado e posterior aplicado ao pagamento — nunca o embuta no valor armazenado, ou suas transações eletrônicas e em dinheiro vão divergir.

Formatação é o último passo, não o cálculo

Feita a aritmética, entregue a exibição a um formatador ciente de locale. O Intl.NumberFormat já conhece a contagem de decimais, a posição do símbolo e os separadores de cada moeda:

function formatMinor(amountMinor, currency, locale = "en-US") {
  const exp = exponentFor(currency);
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(amountMinor / 10 ** exp);
}

formatMinor(1999, "USD");            // "$19.99"
formatMinor(2943, "JPY", "ja-JP");   // "¥2,943"
formatMinor(6115, "KWD");            // "KWD 6.115"
formatMinor(1847, "EUR", "de-DE");   // "18,47 €"

Duas notas práticas. Primeiro, instâncias de Intl.NumberFormat são caras de construir — mantenha uma em cache por par locale-moeda em vez de criar uma por linha. Segundo, a divisão por 10 ** exp na última linha é o único lugar em que um float deveria tocar um valor monetário, e apenas porque o resultado vira string imediatamente.

Juntando tudo com a API da Finexly

Busque a taxa em precisão total, converta uma vez, arredonde uma vez e guarde a taxa usada:

curl "https://api.finexly.com/v1/latest?base=USD&symbols=JPY,KWD,EUR" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "success": true,
  "base": "USD",
  "timestamp": 1755244800,
  "rates": {
    "JPY": 147.2150,
    "KWD": 0.30590,
    "EUR": 0.9241
  }
}
async function quote(amountMinor, from, to) {
  const res = await fetch(
    `https://api.finexly.com/v1/latest?base=${from}&symbols=${to}`,
    { headers: { Authorization: `Bearer ${process.env.FINEXLY_API_KEY}` } }
  );
  const data = await res.json();
  const rate = data.rates[to];

  return {
    amount_minor: convertMinor(amountMinor, from, to, rate),
    currency: to,
    rate,                              // persist the exact rate used
    rate_timestamp: data.timestamp,    // and when it was captured
  };
}

await quote(1999, "USD", "JPY");
// { amount_minor: 2943, currency: "JPY", rate: 147.215, rate_timestamp: 1755244800 }

Guardar rate e rate_timestamp na linha da transação é o que torna uma disputa respondível seis meses depois. Detalhes completos de endpoints e parâmetros estão na documentação da API Finexly, e se você faz cache de taxas entre requisições, nossas notas sobre cache e tratamento de erros cobrem os trade-offs de defasagem.

Um checklist de testes

Bugs de dinheiro se escondem nos casos que ninguém testa. No mínimo, cubra:

  1. Um destino sem decimais — converta para JPY ou KRW e verifique que o resultado não tem parte fracionária.
  2. Um destino com três decimais — converta para KWD ou BHD e verifique que as três casas sobrevivem.
  3. Metades exatas — verifique o modo de arredondamento escolhido, nos dois sentidos, incluindo negativos.
  4. Desvio de ida e volta — converta USD → EUR → USD e verifique que o resultado está dentro de uma unidade menor, não que seja igual.
  5. Invariância da alocação — verifique que as partes divididas sempre somam exatamente o total, de 1 a 100 partes.
  6. Códigos de moeda desconhecidos — verifique que o código lança erro em vez de assumir duas casas silenciosamente.
  7. Valores muito grandes — verifique que não há perda de precisão além de Number.MAX_SAFE_INTEGER em JavaScript; use BigInt se lidar com IDR ou VND em escala.

Perguntas frequentes

Quantas casas decimais tem cada moeda?

A maioria tem duas. Cerca de duas dúzias não têm nenhuma — incluindo JPY, KRW, VND, CLP e ISK — e sete têm três: KWD, BHD, OMR, JOD, TND, IQD e LYD. A ISO 4217 é a fonte autoritativa, mas confira também a tabela do seu provedor de pagamentos, já que alguns se desviam por razões operacionais.

Devo arredondar taxas de câmbio ou valores convertidos?

Somente valores convertidos. Mantenha a taxa na precisão total que seu provedor retorna, multiplique e depois arredonde o resultado uma única vez para a unidade menor da moeda de destino. Arredondar uma taxa antes de multiplicar introduz um erro proporcional ao tamanho da transação.

Qual a diferença entre half-up e arredondamento do banqueiro?

Half-up sempre afasta do zero uma metade exata (2,5 → 3). O arredondamento do banqueiro — metade para o par — a envia ao dígito par mais próximo (2,5 → 2, 3,5 → 4), o que remove o viés sistemático de alta ao agregar muitos valores. Use half-up para preços exibidos a clientes e half-even para contabilidade e relatórios.

Por que meus itens convertidos não somam o total convertido?

Porque cada linha foi arredondada de forma independente e os erros se acumulam. Arredonde o total uma vez e depois aloque esse total entre as linhas usando uma divisão por maior resto. As partes então somarão o todo por construção.

Posso simplesmente armazenar dinheiro como float com duas casas?

Não. O ponto flutuante binário não representa exatamente valores como 0,1, então os erros se acumulam em somas e multiplicações e acabam invertendo uma decisão de arredondamento. Armazene inteiros em unidades menores, ou use um tipo decimal exato. Isso não é uma preocupação teórica — é a causa raiz mais comum de falhas de conciliação por um centavo.

Obtenha taxas em precisão total

O arredondamento correto começa com uma taxa em que você confia e com a precisão que você não jogou fora. Pegue sua chave de API Finexly gratuita — sem cartão de crédito. Comece com 1.000 requisições grátis por mês em mais de 170 moedas, use o conversor de moedas para conferir suas contas e revise os planos de preços quando seu volume crescer.

Vlado Grigirov

Senior Currency Markets Analyst & Financial Strategist

Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...

View full profile →

Compartilhar este artigo