How to Access Brazilian CVM Filings with Python

If you build data pipelines for Brazilian equities, you eventually need filings from the CVM (Comissão de Valores Mobiliários) — annual statements, quarterly reports, and reference forms. The official portals are built for humans, not scripts. apicvm gives you a stable HTTP API to resolve companies, list documents, and download originals from Python.

This guide walks through the full flow with PETR4 and a DFP 2024 filing as concrete examples.

The problem

Brazilian regulatory data is scattered across CVM portals, open-data bulk downloads, and company IR sites. Common pain points for Python developers:

  • No ticker-first API on official open data — you get ZIP/CSV dumps, not GET /filings?ticker=PETR4.
  • Scraping breaks when portal HTML or CAPTCHAs change.
  • Metadata is messy — matching a PDF to the right company, year, and filing type takes manual work.

You need a programmatic path: resolve the company once, filter documents by type and year, download the file you chose.

Background: CVM filing types

The CVM requires Brazilian public companies to file standardized documents. The ones you will use most often:

Type Name Cadence Typical use
DFP Demonstrações Financeiras Padronizadas Annual Full-year financials
ITR Informações Trimestrais Quarterly Interim financials
FRE Formulário de Referência Annual (updated) Company profile, governance, risks

apicvm exposes these as filterable document types on GET /v1/documents.

Setup

You need an API key and the base URL:

export APICVM_URL='https://apicvm.dev'
export APICVM_KEY='apicvm_...'   # from /signup

All business endpoints require authentication via Authorization: Bearer or X-API-Key.

Step 1: Resolve the company

Start with a ticker, CNPJ, or company name. Resolution returns a single company or an ambiguity error with candidates.

curl -H "Authorization: Bearer $APICVM_KEY" \
  "$APICVM_URL/v1/companies/resolve?query=PETR4&by=ticker"

Response shape (simplified):

{
  "id": 123,
  "name": "PETROBRAS",
  "cnpj": "33000167000101",
  "sector": "Petróleo, Gás e Biocombustíveis",
  "tickers": [
    { "id": 1, "idCompany": 123, "ticker": "PETR4", "tickerClass": "PN" }
  ]
}

If multiple companies match, the API returns 409 AMBIGUOUS_RESULT with details.candidates[] — narrow your query or pass by=cnpj.

Step 2: List documents

Filter by ticker, document type, and year:

curl -H "Authorization: Bearer $APICVM_KEY" \
  "$APICVM_URL/v1/documents?ticker=PETR4&type=DFP&year=2024&perPage=20"

Each item in data[] includes:

  • id (UUID) — use this for download or text extraction
  • type, year, dateRef, name
  • nested company with tickers

Pick a document explicitly. The API does not auto-select when filters match multiple results.

Step 3: Download the PDF

Stream the original file from the bucket:

curl -OJ -H "Authorization: Bearer $APICVM_KEY" \
  "$APICVM_URL/v1/documents/<document-id>/file"

The response includes Content-Type (usually application/pdf) and Content-Disposition with the filename.

Full Python example

import os
import requests

BASE = os.environ["APICVM_URL"]
HEADERS = {"Authorization": f"Bearer {os.environ['APICVM_KEY']}"}


def apicvm_get(path: str, **params):
    r = requests.get(f"{BASE}{path}", headers=HEADERS, params=params)
    r.raise_for_status()
    return r.json()


# 1. Resolve company
company = apicvm_get("/v1/companies/resolve", query="PETR4", by="ticker")
print(f"Resolved: {company['name']} (id={company['id']})")

# 2. List DFP filings for 2024
docs = apicvm_get(
    "/v1/documents",
    ticker="PETR4",
    type="DFP",
    year=2024,
    perPage=20,
)
if not docs["data"]:
    raise SystemExit("No DFP 2024 documents found for PETR4")

document = docs["data"][0]
doc_id = document["id"]
print(f"Selected: {document['type']} {document['year']} — {document['name']}")

# 3. Download PDF
file_resp = requests.get(
    f"{BASE}/v1/documents/{doc_id}/file",
    headers=HEADERS,
    stream=True,
)
file_resp.raise_for_status()

filename = f"petr4-dfp-{document['year']}.pdf"
with open(filename, "wb") as f:
    for chunk in file_resp.iter_content(chunk_size=8192):
        f.write(chunk)
print(f"Saved {filename}")

For page-level markdown instead of raw PDF bytes, see Extract Text from CVM PDFs for AI Agents.

Pagination and filters

List endpoints support page, perPage (max 50), field, and order. Default document sort is year desc, then dateRef desc.

Useful filters on GET /v1/documents:

  • types=DFP,ITR — multiple types in one call
  • cnpj or companyId — when you already resolved the company
  • search — partial match on document name or type

Current limitations

  • Corpus coverage — apicvm serves companies and documents already ingested in the Hold database. Not every listed B3 company may be available yet.
  • No automatic document selection — you must pick a document.id from list results.
  • Rate limit — 60 requests per minute per API key by default (see X-RateLimit-* headers).
  • Ingestion lag — new CVM filings appear after the upstream pipeline processes them; this is not a real-time feed.

Next steps

Ready to integrate?

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