A taxa de câmbio é uma daquelas grandezas que parecem inofensivas («só um número»), mas que, tratadas sem cuidado, viram uma fonte de erros difíceis de rastrear: diferenças de centavos, um nominal mal aplicado, a taxa «perdida» do fim de semana, o erro de arredondamento acumulado no relatório anual. Este artigo é um panorama prático de onde obter as taxas de câmbio (o Banco Central do Brasil, outros bancos centrais e os serviços internacionais), como extraí-las em cinco linguagens, em que tipo de dados armazená-las e por que as taxas dos bancos comerciais são uma história à parte, com seus próprios agregadores.
1. De onde saem as taxas de câmbio
Convém separar desde o início dois tipos de taxa radicalmente distintos:
A taxa oficial ou de referência do banco central. Um único valor por data, sem compra/venda. É a taxa «contábil»: com ela se calculam impostos, tarifas aduaneiras, contabilidade e contratos — no Brasil, esse papel cabe à PTAX, divulgada pelo Banco Central do Brasil. É estável, publicada segundo um calendário (em geral uma vez ao dia) e quase todos os bancos centrais oferecem uma fonte gratuita e legível por máquina.
As taxas dos bancos comerciais e das casas de câmbio. Cada banco pratica a sua própria taxa de compra e de venda, com o seu spread; ela muda ao longo do dia, difere da oficial e depende da instituição, da cidade, do valor e de a operação ser em espécie ou por transferência. Aqui não há fonte única: disso cuidam os agregadores (seção 6).
Para a maioria das tarefas (contabilidade, preços multimoeda, conversores), a taxa oficial de referência basta. Se a tarefa é «mostrar ao usuário onde comprar dólares mais barato», são necessárias as taxas bancárias.
2. O Banco Central do Brasil e outros bancos centrais
Quase todos os bancos centrais publicam seus dados de graça e sem chave. Os formatos variam: em uns casos um JSON caprichado, em outros XML, às vezes CSV.
| Fonte | O que publica | Acesso | Formato |
|---|---|---|---|
| Banco Central do Brasil — PTAX | dólar e demais moedas frente ao BRL, cotações de compra e venda, boletins ao longo do dia útil e fechamento | API pública de dados abertos do BCB (serviço Olinda/OData), sem chave | JSON/CSV |
| BCE — taxas de referência do euro | ~30 moedas frente ao EUR, um valor por dia útil | https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml, sem chave |
XML |
| BCE — histórico | eurofxref-hist-90d.xml (últimos 90 dias) e eurofxref-hist.xml (série desde 1999) |
sem chave | XML/CSV |
| NBP (Banco Nacional da Polônia) | tabelas A/B (taxa média) e C (compra/venda) | api.nbp.pl, sem chave |
JSON/XML |
| Banco do México | taxa de câmbio FIX e séries históricas | API SIE, com token gratuito | JSON |
| Banco do Canadá | séries de taxas de câmbio | API Valet, sem chave | JSON/CSV |
Os endpoints são citados como estavam na redação do artigo. Os bancos mudam sua infraestrutura de tempos em tempos, então antes de ir para produção convém conferi-los na página oficial para desenvolvedores de cada instituição.
A que prestar atenção nos dados dos bancos centrais:
- O nominal (nominal / scale / quant). A taxa nem sempre é publicada por 1 unidade: muitas fontes cotam as moedas de baixo valor unitário por 10, 100 ou 1000 unidades (é comum ver isso com o iene japonês ou o forinte húngaro). Ignorar esse campo é o erro clássico que devolve uma taxa inflada 100 vezes.
- A codificação. Nem todos os feeds chegam em UTF-8: ainda há fontes que publicam XML ou CSV em codificações legadas (windows-1252, ISO-8859-1). Leia-as «como estão» e você recebe caracteres corrompidos nos nomes.
- O separador decimal. Algumas fontes separam a parte decimal com vírgula (
74,1234) em vez de ponto, sobretudo nas exportações CSV. - O calendário. O Banco Central do Brasil divulga a PTAX apenas em dias úteis, em boletins ao longo do dia e com o valor de fechamento no fim da tarde; o BCE publica suas taxas de referência uma vez por dia útil, por volta das 16:00 CET. Em fins de semana e feriados não há valor novo. Outros bancos centrais fixam a taxa na véspera ou após o pregão, cada um com suas próprias regras.
3. APIs internacionais
Quando se precisa de taxas cruzadas, de muitas moedas ou de uma única fonte «tudo frente a USD/EUR», os serviços internacionais são mais cômodos.
| Serviço | Fonte de dados | Chave | Limite gratuito | Notas |
|---|---|---|---|---|
Frankfurter (api.frankfurter.dev) |
BCE | não precisa | sem limites mensais (só proteção antiabuso) | ~30 moedas, histórico desde 1999, open source, dá para auto-hospedar |
BCE direto (arquivo eurofxref-daily.xml em ecb.europa.eu) |
BCE | não precisa | sim | XML da fonte primária, tudo frente ao EUR |
NBP (Polônia, api.nbp.pl) |
Banco Nacional da Polônia | não precisa | sim | a tabela C inclui compra/venda |
| exchangerate-api.com | mistura de vários bancos centrais | necessária | ~1500/mês | 160+ moedas, ponto médio calculado |
| Open Exchange Rates | agregado | necessária | 1000/mês | no plano gratuito, só base USD |
| Fixer.io / currencylayer (apilayer) | BCE e outros | necessária | ~100/mês | |
| exchangerate.host (apilayer) | agregado | necessária | plano gratuito limitado | conversão e histórico; exige chave desde a migração para a apilayer |
| currencyapi.com | agregado | necessária | ~300/mês | inclui cripto |
| Twelve Data / Alpha Vantage | cotações de mercado | necessária | limites | forex intradiário, não a taxa «contábil» |
A nuance-chave dos agregadores internacionais: suas taxas são um ponto médio indicativo (midpoint, sem spread). Servem muito bem para converter preços de forma orientativa em e-commerce ou para dashboards, mas não servem para operar no mercado de câmbio real nem para calcular o valor exato que um banco cobrará em uma conversão. Para o real, o valor «contábil» continua sendo a PTAX do Banco Central do Brasil — trate os pontos médios como referência. O Frankfurter e o BCE, além disso, só publicam taxas nos dias úteis: uma consulta para o dia 1º de janeiro devolve a taxa do último dia útil, e a resposta inclui o campo date com a data real da taxa — guie-se por esse campo, não pela data solicitada.
4. Cinco soluções em linguagens diferentes
Para mostrar a variedade de fontes e linguagens, cada exemplo aponta para uma fonte ou um recorte de dados diferente. Em todos se insiste deliberadamente em duas coisas: respeitar o nominal ou a base de cotação e armazenar o valor sem float binário.
4.1. PHP — BCE (XML, namespaces, BCMath)
<?php
declare(strict_types=1);
/**
* Devolve as taxas de referência do BCE frente ao euro como strings.
* Strings + BCMath: para não perder precisão com float.
*/
function fetchEcbRates(): array
{
$raw = file_get_contents('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml');
if ($raw === false) {
throw new RuntimeException('Não foi possível obter os dados do BCE');
}
$xml = simplexml_load_string($raw);
if ($xml === false) {
throw new RuntimeException('Erro ao parsear o XML');
}
// O feed usa um namespace padrão: é preciso registrá-lo para o XPath
$xml->registerXPathNamespace('e', 'http://www.ecb.int/vocabulary/2002-08-01/eurofxref');
$rates = [];
foreach ($xml->xpath('//e:Cube[@currency]') as $cube) {
$code = (string) $cube['currency']; // USD, GBP, JPY...
$value = (string) $cube['rate']; // unidades de moeda por 1 EUR
$rates[$code] = $value; // guardamos a string como está
}
return $rates;
}
$rates = fetchEcbRates();
echo "EUR->USD: {$rates['USD']}\n";
// Taxa cruzada USD->JPY com aritmética de strings (sem float)
echo 'USD->JPY: ' . bcdiv($rates['JPY'], $rates['USD'], 6) . "\n";Aqui o ilustrativo é duplo. Primeiro, o namespace: sem registerXPathNamespace, a consulta XPath devolve um resultado vazio mesmo com os dados ali. Segundo, o bcdiv: o BCE cota tudo frente ao euro, então qualquer taxa cruzada sai de uma divisão, e fazê-la com aritmética de strings evita passar por float. Requer a extensão bcmath.
4.2. Python — NBP (JSON, Decimal)
from decimal import Decimal, getcontext
import requests
getcontext().prec = 28 # margem de precisão de sobra
def fetch_nbp_rates() -> dict[str, Decimal]:
"""Taxas médias oficiais do Banco Nacional da Polônia (tipo Decimal)."""
resp = requests.get(
"https://api.nbp.pl/api/exchangerates/tables/A/",
params={"format": "json"},
timeout=10,
)
resp.raise_for_status()
table = resp.json()[0] # a tabela A chega como lista com um único elemento
rates: dict[str, Decimal] = {}
for item in table["rates"]:
code = item["code"] # USD, EUR, CHF...
rates[code] = Decimal(str(item["mid"])) # taxa média da tabela A
return rates
if __name__ == "__main__":
rates = fetch_nbp_rates()
print(f"USD: {rates['USD']:.4f} PLN")
print(f"EUR: {rates['EUR']:.4f} PLN")O ponto essencial é Decimal(str(value)), e não Decimal(value). Se o número já chegou como float, envolvê-lo em str fixa exatamente a representação decimal que vinha no JSON. A tabela C da mesma API, aliás, publica taxas de compra e venda (bid/ask), útil quando você precisa do spread além da taxa média.
4.3. JavaScript / Node.js — ExchangeRate-API (JSON, fetch nativo)
// Node 18+: o fetch já vem integrado
async function fetchOpenErApiRates() {
const res = await fetch("https://open.er-api.com/v6/latest/EUR");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); // atenção: aqui os números JSON já são double
if (data.result !== "success") throw new Error("Resposta inesperada da API");
return {
base: data.base_code, // "EUR"
updated: data.time_last_update_utc, // data real da última atualização
rates: data.rates, // { USD: ..., BRL: ..., ... }
};
}
fetchOpenErApiRates().then(({ base, updated, rates }) => {
console.log(`Base ${base}, atualizado em: ${updated}`);
console.log(`EUR->USD: ${rates.USD}`);
console.log(`EUR->BRL: ${rates.BRL}`);
});É o endpoint aberto (sem chave, com atribuição ao provedor) do exchangerate-api.com, o serviço que já vimos na tabela. O JavaScript não tem um tipo decimal nativo: number é um double IEEE 754, e assim que res.json() parseia a resposta, os valores já vivem nesse formato. Para exibir uma taxa de câmbio, basta; para cálculos monetários usa-se uma biblioteca como decimal.js ou big.js alimentada com strings e, se for necessária exatidão bit a bit, conserva-se o corpo bruto da resposta (res.text()) e extrai-se o literal de lá.
4.4. Go — histórico do BCE (XML, tipagem estrita)
package main
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
)
// Estruturas para o XML do BCE: elemento Cube aninhado em três níveis
type Envelope struct {
Days []Day `xml:"Cube>Cube"`
}
type Day struct {
Date string `xml:"time,attr"` // data da taxa: 2025-03-20
Rates []Rate `xml:"Cube"`
}
type Rate struct {
Currency string `xml:"currency,attr"` // USD, GBP...
Value string `xml:"rate,attr"` // unidades por 1 EUR
}
func fetchEcbHistory() ([]Day, error) {
url := "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml"
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var env Envelope
if err := xml.Unmarshal(body, &env); err != nil {
return nil, err
}
return env.Days, nil
}
func main() {
days, err := fetchEcbHistory()
if err != nil {
panic(err)
}
latest := days[0] // o feed chega ordenado do dia mais recente para o mais antigo
fmt.Printf("Taxas de %s:\n", latest.Date)
for _, r := range latest.Rates {
if r.Currency == "USD" || r.Currency == "GBP" {
// guardamos a string bruta; para a aritmética, shopspring/decimal
fmt.Printf("EUR->%s: %s\n", r.Currency, r.Value)
}
}
}Go não traz um tipo decimal na biblioteca padrão, então a taxa de câmbio fica guardada como string e, para os cálculos, recorre-se a github.com/shopspring/decimal. O arquivo de 90 dias complementa o exemplo de PHP com o recorte «histórico»: com uma única requisição obtém-se a série recente completa, ideal para popular o banco na primeira vez. Leve em conta que a série não contém linhas de fins de semana nem feriados: ao buscar «a taxa do dia X», esteja preparado para recuar até o dia útil anterior.
4.5. C# / .NET — Frankfurter (BCE, fonte internacional, decimal)
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public record FrankfurterResponse(
[property: JsonPropertyName("base")] string Base,
[property: JsonPropertyName("date")] string Date,
[property: JsonPropertyName("rates")] Dictionary<string, decimal> Rates
);
public static class CurrencyClient
{
private static readonly HttpClient Http = new();
public static async Task<FrankfurterResponse> FetchEcbRatesAsync(string baseCcy = "EUR")
{
var url = $"https://api.frankfurter.dev/v1/latest?base={baseCcy}";
return await Http.GetFromJsonAsync<FrankfurterResponse>(url)
?? throw new InvalidOperationException("Resposta vazia do Frankfurter");
}
}
class Program
{
static async Task Main()
{
var data = await CurrencyClient.FetchEcbRatesAsync("USD");
Console.WriteLine($"Data da taxa: {data.Date}"); // data real do BCE
Console.WriteLine($"USD->EUR: {data.Rates["EUR"]}");
Console.WriteLine($"USD->GBP: {data.Rates["GBP"]}");
}
}O System.Text.Json desserializa os números JSON diretamente para decimal (lendo o literal de texto), então a precisão não se perde. No .NET, decimal é o tipo correto tanto para as taxas de câmbio quanto para o dinheiro.
5. Em que tipo de dados armazenar a taxa de câmbio
É, provavelmente, a grande questão técnica do tema — e onde mais se erra.
Por que não float/double
Os números binários de ponto flutuante (IEEE 754) não conseguem representar com exatidão a maioria das frações decimais. 0.1 + 0.2 não é igual a 0.3. Com uma única taxa de câmbio isso não aparece, mas ao multiplicar por valores, reconverter várias vezes e agregar por períodos, os erros se acumulam — e no relatório financeiro surgem diferenças de centavos inexplicáveis que não passam na conciliação. Para dinheiro e taxas de câmbio, float/double são proibidos.
O que usar
| Nível | Escolha correta |
|---|---|
| Banco de dados | DECIMAL / NUMERIC com precision e scale fixos |
| Python | decimal.Decimal |
| PHP | BCMath / strings (ou uma biblioteca Money) |
| Java / C# | BigDecimal / decimal |
| Go | github.com/shopspring/decimal |
| JavaScript | decimal.js / big.js (armazenar como string) |
Quantas casas decimais
As taxas costumam ser publicadas com 4--6 casas decimais, mas as moedas com números grandes por unidade (IDR, COP, CLP) produzem partes inteiras volumosas. Um compromisso seguro para o banco de dados é NUMERIC(20, 6); para a «taxa por 1 unidade» normalizada às vezes se reserva mais precisão, por exemplo NUMERIC(24, 10), para que a divisão pelo nominal não perca dígitos.
O que guardar além do próprio número
Uma taxa de câmbio sem contexto não vale nada. O registro mínimo útil contém a fonte, as duas moedas, o nominal, a data de vigência e a classe da taxa:
CREATE TABLE exchange_rates (
id BIGSERIAL PRIMARY KEY,
source VARCHAR(32) NOT NULL, -- 'BCB', 'ECB', 'NBP', 'FRANKFURTER'
base_ccy CHAR(3) NOT NULL, -- moeda em que a taxa se expressa: BRL, EUR, PLN
quote_ccy CHAR(3) NOT NULL, -- moeda cotada: USD, GBP...
nominal INTEGER NOT NULL DEFAULT 1,
rate NUMERIC(20,6) NOT NULL, -- taxa por `nominal` unidades (como na fonte)
rate_per_one NUMERIC(24,10) NOT NULL, -- taxa normalizada por 1 unidade
rate_type VARCHAR(8) NOT NULL DEFAULT 'official', -- official | buy | sell
effective_date DATE NOT NULL, -- data em que a taxa vigora
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source, base_ccy, quote_ccy, rate_type, effective_date)
);Convém guardar tanto o rate «bruto» (como a fonte entregou, com o seu nominal) quanto o rate_per_one normalizado: o primeiro serve para conciliar com a fonte primária; o segundo, para os cálculos. Os códigos de moeda, segundo o padrão ISO 4217 (três letras): isso elimina pela raiz o problema das grafias divergentes.
Um apontamento à parte sobre os valores monetários (não as taxas): com frequência eles são armazenados como inteiros em unidades menores — centavos. Ou seja, 19.99 USD =
1999. Isso elimina a aritmética fracionária por completo. Mas as taxas de câmbio não são guardadas assim: elas precisam da fração decimal.
6. Taxas bancárias e agregadores
A taxa oficial de referência é uma só. Mas quem vai trocar moeda vê números muito diferentes: cada banco tem a sua taxa de compra e de venda, com spread, e ela muda ao longo do dia. Reunir as taxas de dezenas de instituições em um só lugar é, precisamente, o trabalho dos agregadores.
Exemplos de fontes desse ecossistema:
- Referências de mercado: XE, Wise ou o próprio buscador do Google mostram a taxa média de mercado — útil como referência, não como preço de balcão.
- Comparadores de envio de dinheiro: serviços como o Monito comparam tarifas e taxas efetivas de vários provedores de remessas.
- Portais financeiros locais: em mercados com várias taxas de câmbio simultâneas, o acompanhamento é feito pela imprensa; o caso clássico é a Argentina, onde portais como Ámbito ou DolarHoy publicam diariamente o dólar oficial, o «blue» e as taxas financeiras.
- Sites de bancos e casas de câmbio: a taxa de balcão de cada instituição é publicada no próprio site e, quando não há API, é extraída do HTML com scraping.
Nuance técnica: muitos agregadores não oferecem uma API aberta e os dados precisam ser extraídos do HTML. Aqui é crítico jogar limpo: respeitar o robots.txt e os termos de uso, não metralhar o site com requisições (rate limiting, cache) e, sempre que possível, citar a fonte. Alguns serviços têm API, mas paga. Os dados dos bancos centrais, por outro lado, em geral podem ser reutilizados livremente.
Se você constrói o seu próprio agregador, uma arquitetura razoável é um conjunto de coletores independentes (um por fonte) → uma camada de normalização (códigos para ISO 4217, taxa por unidade, separação de compra/venda) → um repositório único → a sua própria API por cima. No modelo de dados, à diferença das taxas oficiais, surgem dimensões obrigatórias: a instituição, o tipo de operação (compra/venda, espécie/transferência), às vezes a cidade e o valor, e quase sempre a hora exata da captura, porque essas taxas vivem minutos.
7. Conselhos práticos
- Guarde em cache. Os bancos centrais atualizam a taxa uma vez ao dia (ou em uns poucos boletins, como a PTAX). Bater na API com mais frequência do que os dados mudam é inútil e nocivo: faça cache no lado da aplicação.
- Leve em conta fins de semana e feriados. Em dias não úteis não se publica taxa nova: em geral devolve-se a última. Olhe sempre a data da taxa na resposta, não a solicitada.
- Preveja um plano B (fallback). Qualquer fonte pode cair. Convém ter uma de reserva (por exemplo, uma API internacional como o Frankfurter ou outra fonte oficial) e timeouts com novas tentativas de espera exponencial.
- Normalize na entrada. Recodificar os feeds legados, trocar a vírgula decimal por ponto, dividir pelo nominal, levar os códigos ao ISO 4217: tudo isso é melhor fazer na carga, e guardar no banco só dados limpos.
- Vigie as anomalias. Um salto brusco da taxa, de várias vezes, costuma ser sinal de erro da fonte ou da extração (o nominal esquecido!) antes de um acontecimento real. A verificação simples «desvio frente a ontem não maior que N%» captura a maioria desses casos.
8. Onde se aplica
- E-commerce — preços multimoeda, tarifas localizadas para o comprador.
- Contabilidade — conversão de operações à taxa oficial da data, impostos, tarifas aduaneiras.
- Fintech, carteiras, intercâmbio P2P — conversão e exibição de saldos.
- Analytics e dashboards de BI — unificar receitas multimoeda em uma única moeda.
- Conversores e serviços de viagem — recálculo rápido para o usuário.
- Contratos e faturamento — fixação da taxa na data de emissão da fatura.
Em resumo
O scraping de taxas de câmbio é uma tarefa em que 80% da dificuldade não está na requisição HTTP, e sim nos detalhes: o nominal, a codificação, o separador decimal, os fins de semana e — o mais importante — a escolha de um tipo de dados decimal em vez de float em todo o percurso, do extrator ao banco. Para as taxas oficiais quase sempre existe uma fonte gratuita do próprio banco central — no caso do real, a PTAX do Banco Central do Brasil; para as bancárias são necessários agregadores e um scraping cuidadoso e respeitoso com a fonte. Some o cache e a normalização na entrada, e você obtém dados nos quais dá para confiar para cálculos financeiros.