Analyze Brazil CVM Filings with DuckDB

Want DuckDB CVM filings analysis without building a warehouse first? Pull document list JSON from apicvm, load it into DuckDB, and run SQL over ticker / type / year inventories. DuckDB is local analytics; apicvm is the filings HTTP source.

The problem

Analysts often need:

  • A quick inventory of DFP/ITR/FRE rows across a watchlist
  • Joins between tickers and filing years before downloading PDFs
  • Something lighter than standing up Postgres for a one-off study

pandas and Polars work (see pandas and Polars). DuckDB is useful when the rest of the notebook is already SQL-shaped.

Setup

export APICVM_URL='https://apicvm.dev'
export APICVM_KEY='apicvm_...'
pip install duckdb requests

Pull a document page into DuckDB

import os, json, requests, duckdb

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

resp = requests.get(
    f"{BASE}/v1/documents",
    headers=H,
    params={"ticker": "BRFS3", "type": "DFP", "year": 2024, "perPage": 50},
    timeout=30,
)
resp.raise_for_status()
payload = resp.json()

con = duckdb.connect()
con.execute("CREATE TABLE docs AS SELECT * FROM read_json_auto(?)", [json.dumps(payload["data"])])
print(con.execute("SELECT id, name, type, year FROM docs LIMIT 10").fetchdf())

If read_json_auto complains about shape, dump payload["data"] to a .json file and read_json('docs.json') instead.

Watchlist inventory

import time

TICKERS = ["BRFS3", "JBSS3", "ABEV3"]
rows = []

for t in TICKERS:
    page = 1
    while True:
        r = requests.get(
            f"{BASE}/v1/documents",
            headers=H,
            params={"ticker": t, "type": "FRE", "year": 2024, "page": page, "perPage": 50},
            timeout=30,
        )
        r.raise_for_status()
        body = r.json()
        for d in body["data"]:
            rows.append({**d, "tickerQuery": t})
        if page >= body["meta"]["lastPage"]:
            break
        page += 1
        time.sleep(0.2)  # stay under rate limits

con.execute("CREATE OR REPLACE TABLE fre AS SELECT * FROM read_json_auto(?)", [json.dumps(rows)])
print(
    con.execute(
        """
        SELECT tickerQuery, COUNT(*) AS n
        FROM fre
        GROUP BY 1
        ORDER BY 1
        """
    ).fetchdf()
)

Respect X-RateLimit-* headers when looping. See Handle errors and rate limits.

From inventory to PDF

DuckDB helps you choose which id to download — it does not fetch the PDF itself:

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

Current limitations

  • apicvm returns filing metadata and files; it is not a DuckDB-hosted warehouse.
  • List endpoints max perPage 50 — paginate for full coverage.
  • Do not invent document counts offline; query the API for the current inventory.
  • Markdown extraction is a separate async Pro flow, not a DuckDB table.

Next steps

Ready to integrate?

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