Load Brazil CVM Filings into Google Sheets

Need Google Sheets CVM filings inventory without scraping the CVM portal? Use Apps Script + apicvm: resolve a ticker, list DFP/ITR/FRE rows, and write document UUIDs into a sheet your team can share.

The problem

Analysts often keep filing trackers in Sheets. Manual portal downloads do not scale across a watchlist, and copy-pasted filenames are hard to audit. You want document IDs that stay stable when someone refreshes the sheet.

Setup

  1. Create a Google Sheet with headers: ticker, type, year, document_id, name, date_ref.
  2. Extensions → Apps Script.
  3. Store the API key in Script Properties (APICVM_KEY), not in cell formulas.
# Same key you use elsewhere
export APICVM_KEY='apicvm_...'

Base URL in script: https://apicvm.dev.

Apps Script: list DFP rows

function listDfpToSheet() {
  var props = PropertiesService.getScriptProperties();
  var key = props.getProperty('APICVM_KEY');
  var base = 'https://apicvm.dev';
  var ticker = 'PETR4';
  var url = base + '/v1/documents?ticker=' + encodeURIComponent(ticker)
    + '&type=DFP&year=2024&perPage=20';

  var res = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: { Authorization: 'Bearer ' + key },
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() >= 400) {
    throw new Error(res.getResponseCode() + ' ' + res.getContentText());
  }

  var data = JSON.parse(res.getContentText()).data || [];
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.clearContents();
  sheet.appendRow(['ticker', 'type', 'year', 'document_id', 'name', 'date_ref']);
  data.forEach(function (row) {
    sheet.appendRow([
      ticker,
      'DFP',
      2024,
      row.id,
      row.name,
      row.dateRef || '',
    ]);
  });
}

perPage max is 50. For larger inventories, loop page until meta.lastPage. See Paginate and filter.

Resolve before hard-coding

function resolveTicker(ticker) {
  var key = PropertiesService.getScriptProperties().getProperty('APICVM_KEY');
  var url = 'https://apicvm.dev/v1/companies/resolve?query='
    + encodeURIComponent(ticker) + '&by=ticker';
  var res = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + key },
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() === 404) {
    throw new Error(ticker + ' not in corpus');
  }
  return JSON.parse(res.getContentText());
}

What Sheets is good for (and not)

Fit Not a fit
Shared inventory of document UUIDs Binary PDF storage inside the sheet
Light watchlists for IR / research High-volume ETL (use a backend + Airflow)
Manual review queues Sync markdown extraction (extraction is async via callback)

For desktop Excel workflows, see Excel + Power Query. For BI models, see Power BI.

Rate limits

Apps Script triggers can fan out quickly. Stay under the API key window (X-RateLimit-*) and avoid parallel UrlFetchApp storms across dozens of tickers. Details: Handle errors and rate limits.

Current limitations

  • Sheets is not a document store — download PDFs with curl or a backend using GET /v1/documents/:id/file.
  • Text extraction is Pro-only, async via callback; Student keys get 403 on extract.
  • Corpus coverage varies by company and year — empty lists are not “company never filed”.

Next steps

Ready to integrate?

Get an API key and start querying Brazilian CVM filings programmatically.