Access Brazil CVM Filings with Rust

Need a Rust CVM filings API client for Brazilian public companies? apicvm is plain HTTPS JSON. Use reqwest, a Bearer token, and the /v1/* routes — there is no official Rust crate.

The problem

Rust services that need CVM filings often end up:

  • Shelling out to brittle portal scrapers
  • Parsing local ZIP dumps that do not expose stable document IDs
  • Reimplementing auth and pagination for every language binding

A typed HTTP client against a fixed contract is simpler to keep in CI.

Setup

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

Add to Cargo.toml:

[dependencies]
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Resolve a ticker

use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use serde_json::Value;
use std::env;

fn auth_headers() -> HeaderMap {
    let mut h = HeaderMap::new();
    let key = env::var("APICVM_KEY").expect("APICVM_KEY");
    h.insert(
        AUTHORIZATION,
        HeaderValue::from_str(&format!("Bearer {key}")).unwrap(),
    );
    h
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let base = env::var("APICVM_URL")?;
    let client = reqwest::Client::new();

    let company: Value = client
        .get(format!("{base}/v1/companies/resolve"))
        .query(&[("query", "PETR4"), ("by", "ticker")])
        .headers(auth_headers())
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{company}");
    Ok(())
}

List DFP documents

let docs: Value = client
    .get(format!("{base}/v1/documents"))
    .query(&[
        ("ticker", "PETR4"),
        ("type", "DFP"),
        ("year", "2024"),
        ("perPage", "20"),
    ])
    .headers(auth_headers())
    .send()
    .await?
    .error_for_status()?
    .json()
    .await?;

for row in docs["data"].as_array().unwrap_or(&vec![]) {
    println!("{} {}", row["id"], row["name"]);
}

perPage max is 50. Paginate with page until meta.lastPage. See Paginate and filter CVM documents.

Download a PDF

async fn download_file(
    client: &reqwest::Client,
    base: &str,
    doc_id: &str,
    path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let bytes = client
        .get(format!("{base}/v1/documents/{doc_id}/file"))
        .headers(auth_headers())
        .send()
        .await?
        .error_for_status()?
        .bytes()
        .await?;
    std::fs::write(path, &bytes)?;
    Ok(())
}

File download does not consume extraction credits.

curl cross-check

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

Same contract as Go and curl.

Errors and rate limits

Map status codes in your Result: 401 auth, 404 missing document, 429 rate limit. Read X-RateLimit-Remaining when fan-out across tickers. Details: Handle errors and rate limits.

Current limitations

  • Text extraction (POST /v1/document-text-extractions) is async via HTTPS callback — not a sync Rust return value.
  • perPage max is 50.
  • Corpus coverage depends on ingestion lag; resolve before hard-coding CNPJ.

Next steps

Ready to integrate?

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