CMS e plataformas 14 min de leitura

Scraping de taxas de câmbio: APIs, fontes, código e armazenamento de dados

Coleta de taxas de câmbio: APIs oficiais de bancos centrais, agregadores, scraping de sites bancários e armazenamento do histórico para análise.

EW
Equipe Web-Scraping.biz
Coleta de dados para as demandas do negócio
Publicado: 20 março 2025

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
<?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)

python
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)

javascript
// 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)

go
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)

c#
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:

sql
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.