Você pede a PTAX de uma data, a API responde 200 OK e devolve uma lista vazia. Sem erro, sem aviso. Pior: às vezes ela devolve uma cotação, só que de outro dia. Eu sei como é. Você confere a fórmula, confere o filtro, e o número continua sumindo ou aparecendo errado.
A PTAX do Banco Central quase nunca reclama de parâmetro errado: ela responde com o que achou, e muitas vezes não achou nada. Foi assim que montamos o dashboard de câmbio da Chora API, e é o que este artigo mostra.
Você vai consultar o dólar do dia, voltar ao dia útil anterior quando a data não tem cotação, comparar abertura e fechamento, trazer o histórico inteiro de uma vez e tratar as datas duplicadas. O código em Python foi executado aqui. As consultas em Power Query foram montadas pela documentação oficial, e você testa no seu Power BI Desktop. Bora lá.
Requisitos
- API Olinda, serviço PTAX, versão
v1(protocolo OData). Pública: sem login, sem chave. - Para a versão Power Query: Power BI Desktop ou Excel com Power Query, conector Web com acesso Anônimo.
- Para a versão Python: Python 3.10 ou mais novo, só com a biblioteca padrão (
urllib,json,datetime). - Demo ao vivo: choraapi.com.br/dashboards/cambio-ptax/ busca a mesma API direto do navegador.
Válido em: novembro/2026.
O que é a PTAX
A PTAX é a taxa de câmbio de referência do Banco Central. Desde 01/07/2011, ela é a média aritmética das taxas obtidas em quatro consultas diárias aos dealers de câmbio. A API entrega, para cada dia, cinco boletins: uma abertura, três intermediários e um fechamento. O boletim de fechamento (o "PTAX") é a média dos boletins do dia.
Dá para conferir com os números reais de 06/11/2026, na compra do dólar: (5,5042 + 5,5174 + 5,4882 + 5,4930) ÷ 4 = 5,5007, o mesmo valor do fechamento.
Os recursos que você vai usar:
| Recurso | O que devolve |
|---|---|
CotacaoDolarDia | Compra e venda do dólar em uma data |
CotacaoDolarPeriodo | Compra e venda do dólar, uma linha por dia, em um período |
CotacaoMoedaDia | Os boletins do dia de uma moeda, com paridade e tipoBoletim |
CotacaoMoedaPeriodo | Os boletins de um período de uma moeda |
Moedas | A lista das 10 moedas disponíveis |
1 Chame a função do dia
Os recursos de cotação são funções OData: o nome tem parênteses e o valor de cada parâmetro vai em outro lugar da URL, como alias com @. A data tem três regras: formato MM-DD-AAAA, entre aspas simples, e o mês vem primeiro.
curl -s "https://olinda.bcb.gov.br/olinda/servico/PTAX/versao/v1/odata/CotacaoDolarDia(dataCotacao=@dataCotacao)?@dataCotacao='11-06-2026'&\$format=json"
{"value":[{"cotacaoCompra":5.50070,"cotacaoVenda":5.50130,"dataHoraCotacao":"2026-11-06 13:06:13.966362"}]}
dataHoraCotacao vem como texto, com a hora do fechamento. Os números vêm como número, não como texto.
a data 06-11-2026 não dá erro. A API lê como mês 06, dia 11, e devolve a cotação de 11 de junho de 2026. Para os dias de 1 a 12 de cada mês, dia e mês trocam sem nenhum aviso. Formate a data pelo código (%m-%d-%Y) e não monte a string à mão.
2 Dia sem cotação: volte ao dia útil anterior
Peça a PTAX de um sábado, de um feriado ou de uma data futura e a resposta é 200 com "value":[]. Não é erro: é a API dizendo que não há boletim naquele dia.
Quem precisa da "PTAX vigente" (um contrato que pede a PTAX do dia útil anterior, por exemplo) resolve voltando dia a dia até achar uma cotação. Este script foi executado em 07/11/2026, um sábado:
import json, time, urllib.error, urllib.request
from datetime import date, timedelta
BASE = "https://olinda.bcb.gov.br/olinda/servico/PTAX/versao/v1/odata"
def baixar(url, tentativas=3):
for n in range(1, tentativas + 1):
try:
with urllib.request.urlopen(url, timeout=60) as r:
return json.loads(r.read())["value"]
except urllib.error.HTTPError as e:
if e.code in (502, 503, 504) and n < tentativas:
time.sleep(5 * n)
continue
raise
def mmddaaaa(d): # a PTAX quer MM-DD-AAAA, e a data vai entre aspas simples
return d.strftime("%m-%d-%Y")
def dolar_dia(d):
url = (f"{BASE}/CotacaoDolarDia(dataCotacao=@dataCotacao)"
f"?@dataCotacao='{mmddaaaa(d)}'&$format=json")
return baixar(url) # lista vazia = fim de semana, feriado ou data futura (não é erro)
def ptax_do_dia_ou_anterior(d, limite=10):
for volta in range(limite):
achou = dolar_dia(d - timedelta(days=volta))
if achou:
return d - timedelta(days=volta), achou[0]
raise RuntimeError(f"nenhuma cotação nos {limite} dias até {d}")
dia, cot = ptax_do_dia_ou_anterior(date(2026, 11, 7)) # sábado
print("pedido 07/11/2026 (sábado) ->", dia, cot["cotacaoCompra"], cot["cotacaoVenda"], cot["dataHoraCotacao"][11:16])
pedido 07/11/2026 (sábado) -> 2026-11-06 5.5007 5.5013 13:06
Por que assim:
- A retentativa em 502, 503 e 504 é a mesma defesa que usamos com as APIs do Banco Central: elas falham de vez em quando e passam na segunda tentativa.
- O limite de 10 dias evita um laço infinito se o parâmetro estiver errado. Um feriado prolongado tem poucos dias seguidos sem cotação.
timedeltaem vez de mexer no dia do mês, para o código não quebrar na virada de mês e em 29/02.
Quais dias úteis ficam sem cotação? Em 2026, até 06/11, foram 10: 01/01, 16/02, 17/02, 03/04, 21/04, 01/05, 04/06, 07/09, 12/10 e 02/11. Pelas datas, são os feriados bancários nacionais. Para não depender de uma lista sua de feriados, deixe a API dizer.
3 Abertura × fechamento: os boletins do dia
Para ver os cinco boletins de uma moeda, use CotacaoMoedaDia. Este código foi executado em 07/11/2026 e compara a abertura com o fechamento de 06/11:
def boletins(moeda, d):
url = (f"{BASE}/CotacaoMoedaDia(moeda=@moeda,dataCotacao=@dataCotacao)"
f"?@moeda='{moeda}'&@dataCotacao='{mmddaaaa(d)}'&$format=json")
return sorted(baixar(url), key=lambda r: r["dataHoraCotacao"])
bs = boletins("USD", date(2026, 11, 6))
for b in bs: print(f'{b["tipoBoletim"]:<16} {b["dataHoraCotacao"][11:16]} compra {b["cotacaoCompra"]:.4f} venda {b["cotacaoVenda"]:.4f}')
abertura = bs[0]; fech = bs[-1]
print("variação abertura -> fechamento (venda): %+.4f" % (fech["cotacaoVenda"] - abertura["cotacaoVenda"]))
media = round(sum(b["cotacaoCompra"] for b in bs[:4]) / 4, 4)
print("média dos 4 primeiros boletins (compra):", media, "| fechamento PTAX:", round(fech["cotacaoCompra"], 4))
Abertura 10:02 compra 5.5042 venda 5.5048
Intermediário 11:09 compra 5.5174 venda 5.5180
Intermediário 12:06 compra 5.4882 venda 5.4888
Intermediário 13:06 compra 5.4930 venda 5.4936
Fechamento PTAX 13:06 compra 5.5007 venda 5.5013
variação abertura -> fechamento (venda): -0.0035
média dos 4 primeiros boletins (compra): 5.5007 | fechamento PTAX: 5.5007
O último intermediário e o fechamento saem com o mesmo horário (13:06:13); os microssegundos desempatam, e por isso ordenar por dataHoraCotacao e pegar o último funciona.
o mesmo boletim tem dois nomes. No recurso do dia (CotacaoMoedaDia), o fechamento vem como Fechamento PTAX. No recurso de período (CotacaoMoedaPeriodo), vem como Fechamento. Então $filter=tipoBoletim eq 'Fechamento' funciona no período e devolve vazio no dia, e eq 'Fechamento PTAX' faz o oposto. O swagger oficial fala em "Abertura, Intermediário, Fechamento Interbancário ou Fechamento" e não menciona Fechamento PTAX. Confira o valor que o recurso que você usa devolve.
4 Outras moedas: tipo A e tipo B
Moedas lista as 10 moedas: AUD, CAD, CHF, DKK, EUR, GBP, JPY, NOK, SEK e USD. Cada uma tem um tipo, que muda o sentido da paridade:
- Tipo A (CAD, CHF, DKK, JPY, NOK, SEK, USD): a paridade é a quantidade da moeda por 1 dólar.
- Tipo B (AUD, EUR, GBP): a paridade é a quantidade de dólares por 1 unidade da moeda.
Não calcule a cotação em reais pela paridade: ela já vem pronta em cotacaoCompra e cotacaoVenda. Para o fechamento de 06/11/2026 (consulta CotacaoMoedaDia com $filter=tipoBoletim eq 'Fechamento PTAX'):
EUR (tipo B) paridade 1.1144 / 1.1146 cotação em reais 6.13 / 6.1317
JPY (tipo A) paridade 158.23 / 158.27 cotação em reais 0.03476 / 0.03477
Na prática: o euro vale 1,1144 dólar e custa R$ 6,13. O iene tem 158,23 unidades por dólar e custa R$ 0,03476.
5 O histórico inteiro, sem limite de 10 anos
Quem usa a SGS do Banco Central conhece o limite: desde 26/03/2025, séries diárias aceitam no máximo 10 anos por consulta. A Olinda PTAX não tem esse limite. Este código pede 27 anos em uma chamada só e foi executado em 07/11/2026:
def dolar_periodo(inicio, fim):
mdy = lambda d: d.strftime("%m-%d-%Y")
url = (f"{BASE}/CotacaoDolarPeriodo(dataInicial=@dataInicial,dataFinalCotacao=@dataFinalCotacao)"
f"?@dataInicial='{mdy(inicio)}'&@dataFinalCotacao='{mdy(fim)}'&$format=json")
return baixar(url)
historico = dolar_periodo(date(1999, 1, 1), date(2026, 11, 6)) # 27 anos em um pedido só
datas = [r["dataHoraCotacao"][:10] for r in historico]
print(len(historico), "linhas")
print(datas[0], historico[0]["cotacaoVenda"], "->", datas[-1], historico[-1]["cotacaoVenda"])
print("datas repetidas:", len(datas) - len(set(datas)))
6995 linhas
1999-01-04 1.2078 -> 2026-11-06 5.5013
datas repetidas: 2
A resposta tem cerca de 630 KB e voltou em menos de um segundo. E repare na última linha: 2 datas repetidas. CotacaoDolarPeriodo devolve, em geral, uma linha por dia útil, mas não é garantido. Em 21/12/2001 a mesma linha veio duas vezes, e em 23/04/2025 vieram dois fechamentos que diferem só nos milissegundos (13:06:30.416 e 13:06:30.443). Quem soma ou conta por data duplica esses dois dias.
A correção é guardar uma linha por dia, a última por dataHoraCotacao:
por_dia = {}
for r in sorted(historico, key=lambda r: r["dataHoraCotacao"]):
por_dia[r["dataHoraCotacao"][:10]] = r # a última linha do dia vence
print(len(historico), "linhas ->", len(por_dia), "dias únicos")
6995 linhas -> 6993 dias únicos
É a mesma regra do dashboard: "se a API repetir o dia, vale a última".
a regra de "cinco boletins por dia" vale para os dados recentes. Em 21/12/2001, o dia tinha 18 linhas: uma abertura, 14 intermediários (de meia em meia hora), um fechamento interbancário, um intermediário às 17:59 com venda 0.0 e dois fechamentos PTAX. Se você traz boletins de anos antigos com CotacaoMoedaPeriodo, filtre por tipoBoletim.
6 Power Query: o dólar por período
Conforme a documentação oficial do Web.Contents (link nas referências); não executado aqui. Teste no seu Power BI Desktop antes de publicar.
let
Inicio = #date(2026, 1, 1),
Fim = Date.From(DateTime.LocalNow()),
Mdy = (d as date) as text => "'" & Date.ToText(d, "MM-dd-yyyy", "en-US") & "'",
Fonte = Json.Document(Web.Contents("https://olinda.bcb.gov.br", [
RelativePath = "olinda/servico/PTAX/versao/v1/odata/CotacaoDolarPeriodo(dataInicial=@dataInicial,dataFinalCotacao=@dataFinalCotacao)",
Query = [
#"@dataInicial" = Mdy(Inicio),
#"@dataFinalCotacao" = Mdy(Fim),
#"$format" = "json"
]
])),
Linhas = Fonte[value],
Tabela = if List.IsEmpty(Linhas)
then #table(type table [Data = date, Compra = number, Venda = number], {})
else Table.FromRecords(Linhas),
Tipada = if List.IsEmpty(Linhas) then Tabela else Table.TransformColumnTypes(
Table.RenameColumns(
Table.TransformColumns(Tabela, {{"dataHoraCotacao", each Text.Start(_, 10), type text}}),
{{"dataHoraCotacao", "Data"}, {"cotacaoCompra", "Compra"}, {"cotacaoVenda", "Venda"}}),
{{"Data", type date}, {"Compra", type number}, {"Venda", type number}}, "en-US")
in
Tipada
Por que montar assim:
RelativePatheQueryem vez de concatenar a URL. O serviço do Power BI não atualiza fonte dinâmica, aquela cuja URL só se conhece quando a consulta roda. Com a base fixa no primeiro argumento doWeb.Contents, a atualização agendada funciona.Date.ToText(d, "MM-dd-yyyy", "en-US"). A cultura fixa emen-USgarante o mês na frente mesmo num Power BI em português. É a defesa contra a troca de06-11por11-06do passo 1.- O
if List.IsEmpty. SeLinhasvier vazia (período sem cotação),Table.FromRecordsgera uma tabela sem colunas, e as etapas seguintes falhariam. A tabela vazia com as colunas certas evita isso. Text.Start(_, 10)pega os 10 primeiros caracteres dedataHoraCotacao(2026-11-06) e deixa a data limpa.
Para as duas datas repetidas do passo 5, acrescente ao final um Table.Distinct pela coluna Data. Ele fica com a primeira ocorrência de cada data; como as duas linhas repetidas trazem os mesmos valores de compra e venda, o resultado é o mesmo.
Para os boletins do dia de uma moeda, transforme a consulta em função:
(moeda as text, dia as date) as table =>
let
Mdy = "'" & Date.ToText(dia, "MM-dd-yyyy", "en-US") & "'",
Fonte = Json.Document(Web.Contents("https://olinda.bcb.gov.br", [
RelativePath = "olinda/servico/PTAX/versao/v1/odata/CotacaoMoedaDia(moeda=@moeda,dataCotacao=@dataCotacao)",
Query = [#"@moeda" = "'" & moeda & "'", #"@dataCotacao" = Mdy, #"$format" = "json"]
])),
Tabela = Table.FromRecords(Fonte[value])
in
Tabela
Também não executado. Dois pontos que não consegui testar sem o Power BI: se o Web.Contents aceita os parênteses e o @ dentro do RelativePath e das chaves do Query, e como ele monta a URL final. O que eu verifiquei foi o lado da API: a Olinda aceita a URL com @ e aspas codificadas (%40dataCotacao=%2711-06-2026%27) e com os parênteses codificados.
Erros comuns
Cotação vazia (200 com value vazio):
{"@odata.context":"https://was-p.bcnet.bcb.gov.br/olinda/servico/PTAX/versao/v1/odata$metadata#_CotacaoDolarDia","value":[]}
Foi exatamente essa resposta, executada em 06/11/2026, para: data em formato ISO ('2026-11-06'), moeda em minúscula ('usd'), moeda inexistente ('XXX'), data inicial maior que a final, fim de semana, feriado e data futura. Causa: a função não valida o parâmetro; sem dado, devolve lista vazia. Correção: trate value vazio como "sem cotação", use o formato MM-DD-AAAA e o símbolo em maiúscula (USD).
Data lida com o dia e o mês trocados, sem erro:
@dataCotacao='06-11-2026' -> dataHoraCotacao: 2026-06-11 13:09:12
Causa: a API lê MM-DD-AAAA. Correção: formate a data pelo código, como no passo 1.
Data sem aspas (HTTP 400):
/*{
"codigo" : 400,
"mensagem" : "The part '11-06-2026' is not defined."
}*/
Causa: @dataCotacao=11-06-2026 sem as aspas simples. Correção: @dataCotacao='11-06-2026'.
Recurso inexistente (HTTP 400):
"mensagem" : "Cannot find EntitySet, Singleton, ActionImport or FunctionImport with name 'CotacaoDolar'."
Causa: faltou Dia ou Periodo no nome do recurso. Correção: CotacaoDolarDia ou CotacaoDolarPeriodo.
Campo com maiúscula errada no filtro (HTTP 400):
"mensagem" : "The property 'tipoboletim', used in a query expression, is not defined in type 'br.gov.bcb.olinda.servico.PTAX.TipoCotacaoMoeda'."
Causa: o OData diferencia maiúscula de minúscula. Correção: tipoBoletim.
Filtro de boletim que não casa (200 vazio): tipoBoletim eq 'Fechamento PTAX' em CotacaoMoedaPeriodo devolveu 0 linhas, e eq 'Fechamento' em CotacaoMoedaDia também. Causa: o fechamento tem dois nomes (passo 3). Correção: use o nome do recurso que você chama.
os erros 400 da Olinda vêm com o JSON entre /* e */. Se o seu código tentar ler direto como JSON, vai dar erro de parse em cima do erro original. Tire o /* e o */ e leia o miolo (é o que fazemos no artigo sobre o Boletim Focus, com o código pronto).
Dica bônus: confira o seu número no dashboard
Montou a consulta no Power BI ou no Python? Abra o dashboard de câmbio, escolha a mesma data e compare os boletins do dia com os da sua consulta. Se a cotação bater, a data está certa. Se vier de outro dia, volte ao passo 1 e confira o formato.
Conclusão
Agora você consulta a PTAX sabendo que ela não grita quando você erra: confere o formato MM-DD-AAAA, trata a lista vazia como dia sem cotação, volta ao dia útil anterior quando precisa e guarda uma linha por dia. E traz 27 anos de histórico de uma vez, sem o limite de 10 anos da SGS.
Se você quer ligar o Power BI a APIs como essa e montar o painel com dados que se atualizam sozinhos, é isso que o curso Chora API ensina, com Linguagem M e DAX no contexto real. Qualquer dúvida, comenta aqui, na comunidade #conectados.
Quer ligar o Power BI a APIs como essas?
Na Formação Chora API você aprende a ligar o Power BI a uma API e montar o painel com dados atualizados, com Linguagem M e DAX no contexto do curso.
Ver a Formação →Referências
Documentação oficial (consultada em 07/11/2026):
- Documentação da API PTAX — Banco Central / Olinda
- Swagger da API PTAX v1 — Banco Central / Olinda
- Taxas de Câmbio - todos os boletins diários — Portal de Dados Abertos do Banco Central
- Dólar comercial (venda e compra) - todos os boletins diários — Portal de Dados Abertos do Banco Central
- Taxa de câmbio - Livre - Dólar americano (venda) - diário — Portal de Dados Abertos do Banco Central (limite de 10 anos da SGS)
- Web.Contents — Microsoft Learn
- Refresh and dynamic data sources — Microsoft Learn