Skip to content

Instantly share code, notes, and snippets.

@danperrout
Last active September 6, 2025 15:00
Show Gist options
  • Save danperrout/b27197056fa38d0d669332647ab89d7a to your computer and use it in GitHub Desktop.
Save danperrout/b27197056fa38d0d669332647ab89d7a to your computer and use it in GitHub Desktop.
API Função TESOURODIRETO Google Sheets
/*
* @return Acesse radaropcoes.com Retorna a cotação atual de um título específico do Tesouro Direto.
* API: https://radaropcoes.com/
* Fonte: https://www.tesourodireto.com.br/titulos/precos-e-taxas.htm
**/
function TESOURODIRETO(bondName, argumento="r") {
let srcURL = "https://api.radaropcoes.com/bonds.json";
let jsondata = UrlFetchApp.fetch(srcURL);
let parsedData = JSON.parse(jsondata.getContentText()).response;
for(let bond of parsedData.TrsrBdTradgList) {
let currBondName = bond.TrsrBd.nm;
if (currBondName.toLowerCase() === bondName.toLowerCase())
if(argumento == "r")
return bond.TrsrBd.untrRedVal;
else
return bond.TrsrBd.untrInvstmtVal;
}
throw new Error("Título não encontrado.");
}
@adelitofarias
Copy link

Comigo também está fora do ar!
Será que eles descontinuaram esse acesso?

@glauberramos
Copy link

Pra mim tbm está fora do ar

@fabiotirapelli
Copy link

fabiotirapelli commented Aug 18, 2025

Achei, mas tive que mudar "um pouco" o código.

`
/**

  • =TESOURODIRETO("Tesouro IPCA+ 2029"; "ti")
  • argumentos suportados:
    • "ti" -> taxa para investir (ex.: "7,02")
    • "tr" -> taxa para resgatar
    • "min" -> aporte mínimo (número em R$)
    • "venc"-> data de vencimento (string ISO)
    • "tipo"-> U/S/M (sem juros, com juros, mensal - Renda+/Educa+)
    • "raw" -> objeto completo do título (JSON)
  • retrocompat:
    • "i" -> retorna aporte mínimo (PU não está no novo endpoint)
    • "r" -> retorna aporte mínimo (PU não está no novo endpoint)
  • Precisa buscar em https://www.tesourodireto.com.br/o/rentabilidade/resgatar
    */
    function TESOURODIRETO(bondName, argumento) {
    bondName = (bondName || "Tesouro IPCA+ 2029").toString().trim();
    argumento = (argumento || "ti").toString().trim().toLowerCase();

var url = "https://www.tesourodireto.com.br/o/rentabilidade/investir";

var resp = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
followRedirects: true,
headers: {
"Accept": "application/json, text/plain, /",
"User-Agent": "Mozilla/5.0 (compatible; AppsScriptFetcher/1.0)",
"Referer": "https://www.tesourodireto.com.br/produtos/dados-sobre-titulos/rendimento-dos-titulos"
}
});

var code = resp.getResponseCode();
if (code !== 200) {
throw new Error("HTTP " + code + " ao acessar o endpoint. Corpo: " + resp.getContentText().slice(0, 300));
}

var text = resp.getContentText("UTF-8");
var data;
try {
data = JSON.parse(text);
} catch (e) {
throw new Error("Falha ao parsear JSON do endpoint: " + e + " | trecho: " + text.slice(0, 300));
}

if (!Array.isArray(data)) {
throw new Error("Formato inesperado: esperado array na raiz.");
}

// Procura o título por nome (case-insensitive); tenta match exato e depois "contém"
var titulo = data.find(function (o) {
return (o.treasuryBondName || "").toLowerCase() === bondName.toLowerCase();
}) || data.find(function (o) {
return (o.treasuryBondName || "").toLowerCase().indexOf(bondName.toLowerCase()) >= 0;
});

if (!titulo) {
throw new Error("Título não encontrado: " + bondName);
}

function parsePercentFromIndexer(str) {
if (!str) return null;
var m = str.match(/([+-]?\d+(?:[.,]\d+)?)\s*%/);
if (!m) return null;
return Number(m[1].replace(",", "."));
}

switch (argumento) {
case "ti":
return parsePercentFromIndexer(titulo.investmentProfitabilityIndexerName);
case "tr":
return parsePercentFromIndexer(titulo.redemptionProfitabilityFeeIndexerName);
case "ti_str":
return titulo.investmentProfitabilityIndexerName || "";
case "tr_str":
return titulo.redemptionProfitabilityFeeIndexerName || "";
case "min":
return Number(titulo.investmentBondMinimumValue);
case "venc":
return titulo.maturityDate || "";
case "tipo":
return titulo.typeReceiptInterest || "";
case "raw":
return JSON.stringify(titulo);
case "i":
case "r":
return (Number(titulo.investmentBondMinimumValue))*100;
default:
return titulo.investmentProfitabilityIndexerName || "";
}
}

`

@Nauahbanda
Copy link

boa tarde, na verdade preciso pegar o PU e não a taxa, alguém conseguiu depois dessa mudança?

@WadjoResende
Copy link

WadjoResende commented Aug 18, 2025

Agora, o PU do resgate só é possível pelo download do CSV no link:

https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv

A boa notícia é que, pelo Google Sheets é possível obter o CSV diretamente na planilha sem necessidade de fazer o download.
Basta colocar a seguinte fórmula na primeira célula da planilha:

=IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true")

Nesse caso, a planilha irá atualizar toda vez que for reaberta, ou, para forçar a atualização é só fazer uma pequena alteração na fórmula, da um enter, voltar o valor correto e dar outro enter.

Para o PU de aplicação, é só usar o link do CSV de investir:
=IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-investir-csv?download=true")

A fórmula IMPORTDATA não faz a divisão de ponto e vírgula, mas aí é só usar a fórmula SPLIT em uma coluna à frente, referenciando as colunas de interesse:
=ARRAYFORMULA(SPLIT(C2:C70; ";"))

@tuliopascoal
Copy link

Achei, mas tive que mudar "um pouco" o código.

` /**

  • =TESOURODIRETO("Tesouro IPCA+ 2029"; "ti")
  • argumentos suportados:
    • "ti" -> taxa para investir (ex.: "7,02")
    • "tr" -> taxa para resgatar
    • "min" -> aporte mínimo (número em R$)
    • "venc"-> data de vencimento (string ISO)
    • "tipo"-> U/S/M (sem juros, com juros, mensal - Renda+/Educa+)
    • "raw" -> objeto completo do título (JSON)
  • retrocompat:
    • "i" -> retorna aporte mínimo (PU não está no novo endpoint)
    • "r" -> retorna aporte mínimo (PU não está no novo endpoint)
  • Precisa buscar em https://www.tesourodireto.com.br/o/rentabilidade/resgatar
    */
    function TESOURODIRETO(bondName, argumento) {
    bondName = (bondName || "Tesouro IPCA+ 2029").toString().trim();
    argumento = (argumento || "ti").toString().trim().toLowerCase();

var url = "https://www.tesourodireto.com.br/o/rentabilidade/investir";

var resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true, followRedirects: true, headers: { "Accept": "application/json, text/plain, /", "User-Agent": "Mozilla/5.0 (compatible; AppsScriptFetcher/1.0)", "Referer": "https://www.tesourodireto.com.br/produtos/dados-sobre-titulos/rendimento-dos-titulos" } });

var code = resp.getResponseCode(); if (code !== 200) { throw new Error("HTTP " + code + " ao acessar o endpoint. Corpo: " + resp.getContentText().slice(0, 300)); }

var text = resp.getContentText("UTF-8"); var data; try { data = JSON.parse(text); } catch (e) { throw new Error("Falha ao parsear JSON do endpoint: " + e + " | trecho: " + text.slice(0, 300)); }

if (!Array.isArray(data)) { throw new Error("Formato inesperado: esperado array na raiz."); }

// Procura o título por nome (case-insensitive); tenta match exato e depois "contém" var titulo = data.find(function (o) { return (o.treasuryBondName || "").toLowerCase() === bondName.toLowerCase(); }) || data.find(function (o) { return (o.treasuryBondName || "").toLowerCase().indexOf(bondName.toLowerCase()) >= 0; });

if (!titulo) { throw new Error("Título não encontrado: " + bondName); }

function parsePercentFromIndexer(str) { if (!str) return null; var m = str.match(/([+-]?\d+(?:[.,]\d+)?)\s*%/); if (!m) return null; return Number(m[1].replace(",", ".")); }

switch (argumento) { case "ti": return parsePercentFromIndexer(titulo.investmentProfitabilityIndexerName); case "tr": return parsePercentFromIndexer(titulo.redemptionProfitabilityFeeIndexerName); case "ti_str": return titulo.investmentProfitabilityIndexerName || ""; case "tr_str": return titulo.redemptionProfitabilityFeeIndexerName || ""; case "min": return Number(titulo.investmentBondMinimumValue); case "venc": return titulo.maturityDate || ""; case "tipo": return titulo.typeReceiptInterest || ""; case "raw": return JSON.stringify(titulo); case "i": case "r": return (Number(titulo.investmentBondMinimumValue))*100; default: return titulo.investmentProfitabilityIndexerName || ""; } }

`

Obrigado por postar essa solução!
Porém, tentei aqui mas só recebo Error 406, vide abaixo.
"
Error
Error: HTTP 406 ao acessar o endpoint. Corpo:
"

Está funcionando normalmente para você?

Grato.

@tuliopascoal
Copy link

Agora, o PU do resgate só é possível pelo download do CSV no link:

https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv

A boa notícia é que, pelo Google Sheets é possível obter o CSV diretamente na planilha sem necessidade de fazer o download. Basta colocar a seguinte fórmula na primeira célula da planilha:

=IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true")

Nesse caso, a planilha irá atualizar toda vez que for reaberta, ou, para forçar a atualização é só fazer uma pequena alteração na fórmula, da um enter, voltar o valor correto e dar outro enter.

Para o PU de aplicação, é só usar o link do CSV de investir: =IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-investir-csv?download=true")

A fórmula IMPORTDATA não faz a divisão de ponto e vírgula, mas aí é só usar a fórmula SPLIT em uma coluna à frente, referenciando as colunas de interesse: =ARRAYFORMULA(SPLIT(C2:C70; ";"))

Obrigado pela dica.
Existe uma solução similar a esse (i.e., utilizando formulas GoogleSheets) para resgatar o "ti" (taxa para investir)?

Grato.

@WadjoResende
Copy link

Agora, o PU do resgate só é possível pelo download do CSV no link:
https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv
A boa notícia é que, pelo Google Sheets é possível obter o CSV diretamente na planilha sem necessidade de fazer o download. Basta colocar a seguinte fórmula na primeira célula da planilha:
=IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true")
Nesse caso, a planilha irá atualizar toda vez que for reaberta, ou, para forçar a atualização é só fazer uma pequena alteração na fórmula, da um enter, voltar o valor correto e dar outro enter.
Para o PU de aplicação, é só usar o link do CSV de investir: =IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-investir-csv?download=true")
A fórmula IMPORTDATA não faz a divisão de ponto e vírgula, mas aí é só usar a fórmula SPLIT em uma coluna à frente, referenciando as colunas de interesse: =ARRAYFORMULA(SPLIT(C2:C70; ";"))

Obrigado pela dica. Existe uma solução similar a esse (i.e., utilizando formulas GoogleSheets) para resgatar o "ti" (taxa para investir)?

Grato.

O CSV trás essa informação. Você precisa tratar os dados por causa da formatação de vírgula e ponto e vírgula. Mas, está tudo lá.
É possível usar também uma automação no N8N para já entregar os dados tratados, mas ainda não tive tempo pra fazer isso.

@tuliopascoal
Copy link

=IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true")

Hmmm, eu testei as duas chamadas (rendimento-resgatar-csv e rendimento-investir-csv), mas em nenhuma das duas consegui localizar o 'ti'.
Por exemplo, para IPCA+ 2029 essas são as duas linhas:

Resgatar:
Tesouro IPCA+ 2029;IPCA + 7 | 82%;R$ 3.434 | 69;15/05/2029

Investir:
Tesouro IPCA+ 2029;IPCA + 7 | 70%;R$ 34 | 48;R$ 3.448 | 90;15/05/2029

Talvez eu esteja sendo leigo, mas não consegui visualizar o 'ti'.
Obrigado.

@adelitofarias
Copy link

adelitofarias commented Aug 18, 2025

Adaptei o script do App Script para Google Planilha e está funcionando para meu objetivo de pegar o valor de resgate.

/**

  • Retorna informações de um título do Tesouro Direto a partir do CSV oficial de resgate.
  • @param {string} bondName – Nome do título, ex.: "Tesouro IPCA+ com Juros Semestrais 2035"
  • @param {string} argumento – "Taxa de Investimento" → rendimento anual do título
  •                          "Valor Unitario de Recompra" → preço unitário de resgate
    
  •                          "Vencimento" → data de vencimento
    
  • @return {number|string} – Retorna o valor correspondente ao argumento solicitado
    */
    function TESOURODIRETO(bondName, argumento = 'Taxa de Investimento') {
    // Acessa o cache do script para evitar múltiplas requisições desnecessárias
    const cache = CacheService.getScriptCache();
    const cacheKey = ${bondName}_${argumento}; // Chave única para cache

// Tenta obter o resultado do cache
let cachedResult = cache.get(cacheKey);
if (cachedResult) return JSON.parse(cachedResult); // Se existe no cache, retorna imediatamente

// URL do CSV oficial de resgate do Tesouro Direto
const url = 'https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true';

// Faz a requisição e obtém o conteúdo do CSV como texto
const csvText = UrlFetchApp.fetch(url).getContentText('UTF-8');

// Separa o CSV em linhas, removendo espaços extras e linhas vazias
const linhas = csvText.split(/\r?\n/).map(l => l.trim()).filter(l => l);

// Extrai o cabeçalho (primeira linha) e cria um índice para localizar colunas pelo nome
const headers = linhas[0].split(';').map(h => h.trim());
const idx = {};
headers.forEach((h, i) => idx[h] = i);

// Procura a linha que corresponde ao título solicitado
// Faz uma busca aproximada (case-insensitive) para evitar erros de grafia
const linhaTitulo = linhas.slice(1).find(l => {
const t = l.split(';')[idx['Título']].trim().toLowerCase();
return t.includes(bondName.trim().toLowerCase());
});

// Lança erro caso o título não seja encontrado
if (!linhaTitulo) throw new Error('Título não encontrado no CSV: ' + bondName);

// Divide a linha do título em colunas individuais
const colunas = linhaTitulo.split(';');
let resultado;

// Retorna o valor conforme o argumento solicitado
switch (argumento) {
case 'Taxa de Investimento':
// Retorna o rendimento anual do título (string)
resultado = colunas[idx['Rendimento anual do título']].trim();
break;
case 'Valor Unitario de Recompra':
// Retira 'R$', pontos e converte vírgula para ponto antes de transformar em float
resultado = parseFloat(
colunas[idx['Preço unitário de resgate']]
.replace('R$', '')
.replace(/./g, '')
.replace(',', '.')
.trim()
);
break;
case 'Vencimento':
// Retorna a data de vencimento do título
resultado = colunas[idx['Vencimento do Título']].trim();
break;
default:
throw new Error('Argumento inválido para CSV de resgate.');
}

// Salva o resultado no cache por 1 hora (3600 segundos)
cache.put(cacheKey, JSON.stringify(resultado), 3600);

return resultado;
}

// --- Função de teste ---
function teste() {
// Exemplo de retorno da taxa anual do título
const taxa = TESOURODIRETO('Tesouro IPCA+ com Juros Semestrais 2035', 'Taxa de Investimento');
Logger.log('Taxa de Investimento: ' + taxa);

// Exemplo de valor unitário de recompra
const valorRecompra = TESOURODIRETO('Tesouro IPCA+ com Juros Semestrais 2035', 'Valor Unitario de Recompra');
Logger.log('Valor Unitario de Recompra: ' + valorRecompra);

// Exemplo de data de vencimento
const vencimento = TESOURODIRETO('Tesouro IPCA+ com Juros Semestrais 2035', 'Vencimento');
Logger.log('Vencimento: ' + vencimento);

// Exemplo de argumento inválido (mostra que o erro é tratado)
try {
const venc = TESOURODIRETO('Tesouro IPCA+ com Juros Semestrais 2035', 'Vencimentooooooooooo');
Logger.log('Vencimento: ' + venc);
} catch (e) {
Logger.log('Erro esperado: ' + e.message);
}
}

@WadjoResende
Copy link

=IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true")

Hmmm, eu testei as duas chamadas (rendimento-resgatar-csv e rendimento-investir-csv), mas em nenhuma das duas consegui localizar o 'ti'. Por exemplo, para IPCA+ 2029 essas são as duas linhas:

Resgatar: Tesouro IPCA+ 2029;IPCA + 7 | 82%;R$ 3.434 | 69;15/05/2029

Investir: Tesouro IPCA+ 2029;IPCA + 7 | 70%;R$ 34 | 48;R$ 3.448 | 90;15/05/2029

Talvez eu esteja sendo leigo, mas não consegui visualizar o 'ti'. Obrigado.

É por causa da separação por vírgulas. Você precisa tratar os dados.
Nos exemplos que você pegou:
Resgatar = R$ 3.434 | 69 = 3.434,69
Investir = R$ 3.448 | 90 = 3.448,90

@brvfr1
Copy link

brvfr1 commented Aug 18, 2025

Coloque a linha =vlookup(****;IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-investir-csv?download=true";";");4;0)

onde *** é célula com o nome do título na célula do Google Sheets, que já aparece o valor do título.. se mudar o número 4 do final da fórmula, altera-se os dados a serem carregados do csv...

@tuliopascoal
Copy link

=IMPORTDATA("https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true")

Hmmm, eu testei as duas chamadas (rendimento-resgatar-csv e rendimento-investir-csv), mas em nenhuma das duas consegui localizar o 'ti'. Por exemplo, para IPCA+ 2029 essas são as duas linhas:
Resgatar: Tesouro IPCA+ 2029;IPCA + 7 | 82%;R$ 3.434 | 69;15/05/2029
Investir: Tesouro IPCA+ 2029;IPCA + 7 | 70%;R$ 34 | 48;R$ 3.448 | 90;15/05/2029
Talvez eu esteja sendo leigo, mas não consegui visualizar o 'ti'. Obrigado.

É por causa da separação por vírgulas. Você precisa tratar os dados. Nos exemplos que você pegou: Resgatar = R$ 3.434 | 69 = 3.434,69 Investir = R$ 3.448 | 90 = 3.448,90

Obrigado pela resposta.
Talvez eu referenciei a variável errada, mas os valores que eu estou buscando é a taxa de juros paga no momento.
Por exemplo, para Tesouro IPCA+ 2029 é IPCA + 7,70% (18/08/2025).
Mas o resultado do GoogleSheets não parece ser tão preciso (IPCA +7) ao invés de 7,70.

A variável que eu buscava anteriorment era: 'anulInvstmtRate'.
Grato.

@fabiotirapelli
Copy link

fabiotirapelli commented Aug 19, 2025

Segue uma versão usando os CSVs, assim conseguimos os dados de investimento e de resgate:

`function TESOURODIRETO(bondName, argumento, urlCsv) {
bondName = (bondName || "Tesouro IPCA+ 2029").toString().trim();
argumento = (argumento || "ti").toString().trim().toLowerCase();
var urlInvestir = "https://www.tesourodireto.com.br/documents/d/guest/rendimento-investir-csv?download=true";
var urlResgatar = "https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true";
if (!urlCsv || urlCsv === "") {
if (argumento === "ti" || argumento === "ti_txt" || argumento === "i" || argumento === "i_txt" || argumento === "min") {
urlCsv = urlInvestir;
} else if (argumento === "tr" || argumento === "tr_txt" || argumento === "r" || argumento === "r_txt") {
urlCsv = urlResgatar;
} else {
urlCsv = urlInvestir;
}
}

var resp = UrlFetchApp.fetch(urlCsv, { muteHttpExceptions: true, followRedirects: true });
if (resp.getResponseCode() !== 200) {
throw new Error("HTTP " + resp.getResponseCode() + " ao acessar CSV: " + urlCsv);
}

var text = resp.getContentText("UTF-8");

var rows = Utilities.parseCsv(text, ";");
if (!rows || rows.length < 2) throw new Error("CSV vazio ou sem dados.");

var header = rows[0].map(function(s){ return String(s||"").trim(); });

var investirLayout = header.length >= 5 && header[2].toLowerCase().indexOf("investimento") >= 0;

var IDX = {};
if (investirLayout) {
IDX = { titulo:0, taxa:1, minimo:2, puInvest:3, venc:4, puResg:null };
} else {
IDX = { titulo:0, taxa:1, minimo:null, puInvest:null, venc:3, puResg:2 };
}

var lower = bondName.toLowerCase();
var data = rows.slice(1);
var row = data.find(function(r){ return (r[IDX.titulo]||"").toLowerCase() === lower; }) ||
data.find(function(r){ return (r[IDX.titulo]||"").toLowerCase().indexOf(lower) >= 0; });

if (!row) throw new Error("Título não encontrado: " + bondName);

var taxaTxt = row[IDX.taxa] || "";
var vencTxt = row[IDX.venc] || "";
var minimoTxt = (IDX.minimo != null) ? (row[IDX.minimo] || "") : "";
var puInvTxt = (IDX.puInvest != null) ? (row[IDX.puInvest] || "") : "";
var puResTxt = (IDX.puResg != null) ? (row[IDX.puResg] || "") : "";
var taxaNum = extractPercentNumber(taxaTxt);
var minimo = toNumberCurrency(minimoTxt);
var puInv = toNumberCurrency(puInvTxt);
var puRes = toNumberCurrency(puResTxt);

switch (argumento) {
case "ti": return taxaNum;
case "ti_txt": return taxaTxt;
case "i": return puInv;
case "i_txt": return puInvTxt;
case "min": return minimo;
case "tr": return taxaNum;
case "tr_txt": return taxaTxt;
case "r": return puRes;
case "r_txt": return puResTxt;
case "venc": return vencTxt;
case "raw": return JSON.stringify(objectFromRow(header, row));
default: return taxaNum;
}
}

function toNumberCurrency(brStr){
if (!brStr) return NaN;
var s = String(brStr).replace(/[^\d,.-]/g,"").replace(/./g,"").replace(",",".");
var n = Number(s);
return isNaN(n) ? NaN : n;
}
function extractPercentNumber(txt){
if (!txt) return NaN;
var m = String(txt).match(/(-?\d+(?:,\d+)?)\s*%/);
return m ? Number(m[1].replace(",", ".")) : NaN;
}
function objectFromRow(header, row){
var o = {};
for (var i=0;i<header.length;i++) o[header[i]] = row[i];
return o;
}`

@Nauahbanda
Copy link

bom dia, qual argumento passa para pegar o PU de resgate? obrigado.

@tuliopascoal
Copy link

Segue uma versão usando os CSVs, assim conseguimos os dados de investimento e de resgate:

`function TESOURODIRETO(bondName, argumento, urlCsv) { bondName = (bondName || "Tesouro IPCA+ 2029").toString().trim(); argumento = (argumento || "ti").toString().trim().toLowerCase(); var urlInvestir = "https://www.tesourodireto.com.br/documents/d/guest/rendimento-investir-csv?download=true"; var urlResgatar = "https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true"; if (!urlCsv || urlCsv === "") { if (argumento === "ti" || argumento === "ti_txt" || argumento === "i" || argumento === "i_txt" || argumento === "min") { urlCsv = urlInvestir; } else if (argumento === "tr" || argumento === "tr_txt" || argumento === "r" || argumento === "r_txt") { urlCsv = urlResgatar; } else { urlCsv = urlInvestir; } }

var resp = UrlFetchApp.fetch(urlCsv, { muteHttpExceptions: true, followRedirects: true }); if (resp.getResponseCode() !== 200) { throw new Error("HTTP " + resp.getResponseCode() + " ao acessar CSV: " + urlCsv); }

var text = resp.getContentText("UTF-8");

var rows = Utilities.parseCsv(text, ";"); if (!rows || rows.length < 2) throw new Error("CSV vazio ou sem dados.");

var header = rows[0].map(function(s){ return String(s||"").trim(); });

var investirLayout = header.length >= 5 && header[2].toLowerCase().indexOf("investimento") >= 0;

var IDX = {}; if (investirLayout) { IDX = { titulo:0, taxa:1, minimo:2, puInvest:3, venc:4, puResg:null }; } else { IDX = { titulo:0, taxa:1, minimo:null, puInvest:null, venc:3, puResg:2 }; }

var lower = bondName.toLowerCase(); var data = rows.slice(1); var row = data.find(function(r){ return (r[IDX.titulo]||"").toLowerCase() === lower; }) || data.find(function(r){ return (r[IDX.titulo]||"").toLowerCase().indexOf(lower) >= 0; });

if (!row) throw new Error("Título não encontrado: " + bondName);

var taxaTxt = row[IDX.taxa] || ""; var vencTxt = row[IDX.venc] || ""; var minimoTxt = (IDX.minimo != null) ? (row[IDX.minimo] || "") : ""; var puInvTxt = (IDX.puInvest != null) ? (row[IDX.puInvest] || "") : ""; var puResTxt = (IDX.puResg != null) ? (row[IDX.puResg] || "") : ""; var taxaNum = extractPercentNumber(taxaTxt); var minimo = toNumberCurrency(minimoTxt); var puInv = toNumberCurrency(puInvTxt); var puRes = toNumberCurrency(puResTxt);

switch (argumento) { case "ti": return taxaNum; case "ti_txt": return taxaTxt; case "i": return puInv; case "i_txt": return puInvTxt; case "min": return minimo; case "tr": return taxaNum; case "tr_txt": return taxaTxt; case "r": return puRes; case "r_txt": return puResTxt; case "venc": return vencTxt; case "raw": return JSON.stringify(objectFromRow(header, row)); default: return taxaNum; } }

function toNumberCurrency(brStr){ if (!brStr) return NaN; var s = String(brStr).replace(/[^\d,.-]/g,"").replace(/./g,"").replace(",","."); var n = Number(s); return isNaN(n) ? NaN : n; } function extractPercentNumber(txt){ if (!txt) return NaN; var m = String(txt).match(/(-?\d+(?:,\d+)?)\s*%/); return m ? Number(m[1].replace(",", ".")) : NaN; } function objectFromRow(header, row){ var o = {}; for (var i=0;i<header.length;i++) o[header[i]] = row[i]; return o; }`

Perfeito! Muito obrigado!!

@jcimagem
Copy link

Testei buscando PU com o parâmetro r e retorna valor zerado. Mas se peço com o parâmetro r_txt vêm o valor de resgate em formato texto. Deve ter um erro na função to NumberCurrency mas não sei identificar.

@jcimagem
Copy link

Mudei a função toNumberCurrency e resolveu o retorno do PU em forma de valor (parâmetro r):

function toNumberCurrency(brStr){
if (!brStr) return NaN;
var s = brStr.replace("R$", "").replace(/./g, "").replace(",", ".");
var n = parseFloat(s);
return isNaN(n) ? NaN : n;
}

E adicionei o 4º parâmetro na função principal (updt):
function TESOURODIRETO(bondName, argumento, urlCsv, updt) {

updt é 1 célula com caixa de seleção e quando clico atualiza.

@lturbino-jpg
Copy link

Mudei a função toNumberCurrency e resolveu o retorno do PU em forma de valor (parâmetro r):

function toNumberCurrency(brStr){ if (!brStr) return NaN; var s = brStr.replace("R$", "").replace(/./g, "").replace(",", "."); var n = parseFloat(s); return isNaN(n) ? NaN : n; }

E adicionei o 4º parâmetro na função principal (updt): function TESOURODIRETO(bondName, argumento, urlCsv, updt) {

updt é 1 célula com caixa de seleção e quando clico atualiza.

Continuou não funcionando aqui para o valor, ajustei a célula conforme abaixo e funcionou:

=SUBSTITUTE(TesouroDireto(H173; "r_txt");"R$ ";"")+0

@thiagochaim
Copy link

Boa tarde pessoal,
Estou tentando acompanhar todas as mudanças, mas tenho pouquíssimo conhecimento com o código.
Gostaria de saber se algum de vocês poderia postar o código completo que esteja funcionando no App Script do google depois da atualização do site do tesouro. A única informação que preciso atualmente é o valor de resgate dos títulos. Eu estava usando um script que criava a função TESOURODIRETO(), não sei se é esse que estão usando aqui.

Se for preciso, posso compartilhar o código.

Se puderem ajudar, agradeço.

@WadjoResende
Copy link

WadjoResende commented Aug 25, 2025

Boa tarde pessoal, Estou tentando acompanhar todas as mudanças, mas tenho pouquíssimo conhecimento com o código. Gostaria de saber se algum de vocês poderia postar o código completo que esteja funcionando no App Script do google depois da atualização do site do tesouro. A única informação que preciso atualmente é o valor de resgate dos títulos. Eu estava usando um script que criava a função TESOURODIRETO(), não sei se é esse que estão usando aqui.

Se for preciso, posso compartilhar o código.

Se puderem ajudar, agradeço.

O código que está funcionando é o que o fabiotirapelli postou.
Basta você substituir o código que estava usando pelo código dele e substituir a fórmula na célula que você quer ver o valor de regate pela fórmula abaixo sugerida pelo lturbino-jpg (A4 é a célula que contém o nome do título):

=SUBSTITUTE(TESOURODIRETO(A4; "r_txt");"R$ ";"")+0

Vou colocar aqui o código completo sugerido pelo fabiotirapelli. É só copiar e substituir o código que você estava usando (sem os apóstrofos do início e final):

`function TESOURODIRETO(bondName, argumento, urlCsv) {
bondName = (bondName || "Tesouro IPCA+ 2029").toString().trim();
argumento = (argumento || "ti").toString().trim().toLowerCase();
var urlInvestir = "https://www.tesourodireto.com.br/documents/d/guest/rendimento-investir-csv?download=true";
var urlResgatar = "https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true";
if (!urlCsv || urlCsv === "") {
if (argumento === "ti" || argumento === "ti_txt" || argumento === "i" || argumento === "i_txt" || argumento === "min") {
urlCsv = urlInvestir;
} else if (argumento === "tr" || argumento === "tr_txt" || argumento === "r" || argumento === "r_txt") {
urlCsv = urlResgatar;
} else {
urlCsv = urlInvestir;
}
}

var resp = UrlFetchApp.fetch(urlCsv, { muteHttpExceptions: true, followRedirects: true });
if (resp.getResponseCode() !== 200) {
throw new Error("HTTP " + resp.getResponseCode() + " ao acessar CSV: " + urlCsv);
}

var text = resp.getContentText("UTF-8");

var rows = Utilities.parseCsv(text, ";");
if (!rows || rows.length < 2) throw new Error("CSV vazio ou sem dados.");

var header = rows[0].map(function(s){ return String(s||"").trim(); });

var investirLayout = header.length >= 5 && header[2].toLowerCase().indexOf("investimento") >= 0;

var IDX = {};
if (investirLayout) {
IDX = { titulo:0, taxa:1, minimo:2, puInvest:3, venc:4, puResg:null };
} else {
IDX = { titulo:0, taxa:1, minimo:null, puInvest:null, venc:3, puResg:2 };
}

var lower = bondName.toLowerCase();
var data = rows.slice(1);
var row = data.find(function(r){ return (r[IDX.titulo]||"").toLowerCase() === lower; }) ||
data.find(function(r){ return (r[IDX.titulo]||"").toLowerCase().indexOf(lower) >= 0; });

if (!row) throw new Error("Título não encontrado: " + bondName);

var taxaTxt = row[IDX.taxa] || "";
var vencTxt = row[IDX.venc] || "";
var minimoTxt = (IDX.minimo != null) ? (row[IDX.minimo] || "") : "";
var puInvTxt = (IDX.puInvest != null) ? (row[IDX.puInvest] || "") : "";
var puResTxt = (IDX.puResg != null) ? (row[IDX.puResg] || "") : "";
var taxaNum = extractPercentNumber(taxaTxt);
var minimo = toNumberCurrency(minimoTxt);
var puInv = toNumberCurrency(puInvTxt);
var puRes = toNumberCurrency(puResTxt);

switch (argumento) {
case "ti": return taxaNum;
case "ti_txt": return taxaTxt;
case "i": return puInv;
case "i_txt": return puInvTxt;
case "min": return minimo;
case "tr": return taxaNum;
case "tr_txt": return taxaTxt;
case "r": return puRes;
case "r_txt": return puResTxt;
case "venc": return vencTxt;
case "raw": return JSON.stringify(objectFromRow(header, row));
default: return taxaNum;
}
}

function toNumberCurrency(brStr){
if (!brStr) return NaN;
var s = String(brStr).replace(/[^\d,.-]/g,"").replace(/./g,"").replace(",",".");
var n = Number(s);
return isNaN(n) ? NaN : n;
}
function extractPercentNumber(txt){
if (!txt) return NaN;
var m = String(txt).match(/(-?\d+(?:,\d+)?)\s*%/);
return m ? Number(m[1].replace(",", ".")) : NaN;
}
function objectFromRow(header, row){
var o = {};
for (var i=0;i<header.length;i++) o[header[i]] = row[i];
return o;
}`

@thiagochaim
Copy link

@WadjoResende Muito obrigado. Funcionou perfeitamente.

@gwstavo
Copy link

gwstavo commented Sep 4, 2025

Não funcionou para mim. Copiei o código e a fórmula mas não consegui. Será que estou fazendo algo errado?
@thiagochaim @WadjoResende

@tuliopascoal
Copy link

@gwstavo , talvez seja porque desde hoje o código mais atual que estava funcionando está retornando 403.
O TD deve ter bloqueado os acessos novamente...

Alguém tem alguma novidade?

@thiagochaim
Copy link

Não funcionou para mim. Copiei o código e a fórmula mas não consegui. Será que estou fazendo algo errado? @thiagochaim @WadjoResende

Realmente parou de funcionar novamente.
Que tristeza.

@andretc-cit
Copy link

Vish, tá retornando 403 da url "https://www.tesourodireto.com.br/documents/d/guest/rendimento-resgatar-csv?download=true";, talvez tenham limitado

@tuliopascoal
Copy link

Sim, está fora desde meados da quarta/quinta-feira.
Alguém já descobriu uma forma diferente?

@gabrielgasp
Copy link

Ajustei uma API que eu uso para consumir os CSVs como fonte de dados. Estou deixando ela pública para quem tiver interesse.

Observações

  1. Essa API foi feita para meu uso pessoal, atendendo às minhas próprias necessidades. Não pretendo implementar modificações sob demanda. Está disponível gratuitamente as is.
  2. Como os dados vêm diretamente dos CSVs, não há garantia de que a API continuará funcionando indefinidamente.
  3. O campo investable foi adicionado por mim para indicar se o título ainda está disponível para investimento atualmente na plataforma do Tesouro.
  4. O campo updated_at representa a última vez que a api conseguiu baixar os CSVs para atualizar as informações. Isso não garante que os CSVs na fonte estejam atualizados, pois isso foge do meu controle.
  5. A aplicação tenta atualizar as informações a cada 15 minutos, de segunda a sexta-feira, entre 9h e 19h.

Exemplos de uso

GET https://tesouro.gabrielgaspar.com.br/bonds

{
  "bonds": [
    {
      "name": "Tesouro Selic 2028",
      "investable": true,
      "annual_investment_rate": "SELIC + 0,05%",
      "unitary_investment_value": 17284.36,
      "minimum_investment_amount": 172.84,
      "annual_redemption_rate": "SELIC + 0,06%",
      "unitary_redemption_value": 17280.11,
      "maturity": "01/03/2028"
    },
    {
      "name": "Tesouro Prefixado 2029",
      "investable": false,
      "annual_investment_rate": "",
      "unitary_investment_value": 0,
      "minimum_investment_amount": 0,
      "annual_redemption_rate": "13,33%",
      "unitary_redemption_value": 662.55,
      "maturity": "01/01/2029"
    },
    {
      "name": "Tesouro IPCA+ 2029",
      "investable": true,
      "annual_investment_rate": "IPCA + 7,76%",
      "unitary_investment_value": 3453.16,
      "minimum_investment_amount": 34.53,
      "annual_redemption_rate": "IPCA + 7,88%",
      "unitary_redemption_value": 3439.16,
      "maturity": "15/05/2029"
    },
    ...
  ],
  "updated_at": "2025-09-06T00:11:23-03:00"
}
GET https://tesouro.gabrielgaspar.com.br/bonds/{bondName}
Exemplo: GET https://tesouro.gabrielgaspar.com.br/bonds/tesouro-selic-2028  
(obs: espaços no nome do título são substituídos por hífens)

{
  "bond": {
    "name": "Tesouro Selic 2028",
    "investable": true,
    "annual_investment_rate": "SELIC + 0,05%",
    "unitary_investment_value": 17284.36,
    "minimum_investment_amount": 172.84,
    "annual_redemption_rate": "SELIC + 0,06%",
    "unitary_redemption_value": 17280.11,
    "maturity": "01/03/2028"
  },
  "updated_at": "2025-09-06T00:11:23-03:00"
}
// Exemplo em Google Apps Script
// =GET_TITULO_TESOURO("tesouro-selic-2028")

function GET_TITULO_TESOURO(bondName) {
  // bondName é o nome do título. Ex: tesouro-selic-2028
  const srcURL = `https://tesouro.gabrielgaspar.com.br/bonds/${bondName}`;
  const jsondata = UrlFetchApp.fetch(srcURL);

  if (jsondata.getResponseCode() !== 200) {
    throw new Error("Erro ao buscar título");
  } 

  const parsedData = JSON.parse(jsondata.getContentText());

  // parsedData contém todas as chaves mostradas no exemplo acima.
  // Aqui, por exemplo, retorno o valor unitário de investimento.
  return parsedData.bond.unitary_investment_value;
}

@gabrielgasp
Copy link

gabrielgasp commented Sep 6, 2025

Pra quem quer tentar uma implementação própria pra puxar os dados, a dica que eu posso dar é tentar achar uma forma de fazer a requisição que simule um browser. Pelos meus testes, quando você tenta fazer um fetch direto ou até mesmo um wget, eles conseguem detectar e bloquear (403), mas quando eles acham que o acesso está vindo de um browser tipo chrome eles deixa passar na maioria das vezes.

Essa api ai de cima eu implementei com Golang usando uma lib que consegue fazer o request impersonando o Chrome. Por enquanto tá dando certo.

@afalcaoneto88
Copy link

afalcaoneto88 commented Sep 6, 2025

Consegui puxar o valor (do IPCA+ 2040) no Google Sheets usando:
=VALUE(SUBSTITUTE(REGEXREPLACE(IMPORTXML("https://taxas-tesouro.com/resgatar/tesouro-ipca+-2040/"; "(//span[contains(.,'R$')])[1]");"[^\d.,]";"");".";","))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment