Authenticate to the Brazil CVM Filings API

CVM API authentication is the first step before you resolve companies, list DFP/ITR/FRE filings, or download PDFs. apicvm protects business routes with an API key and accepts two equivalent headers so you can match the style of your HTTP client.

The problem

Unauthenticated calls to /v1/companies/resolve or /v1/documents fail in production when auth is enabled. Teams often mix header names, hard-code keys into repos, or assume demo routes work for every ticker. You need a clear contract: which routes need a key, which headers are valid, and what 401 means.

How apicvm authentication works

When APICVM_AUTH_ENABLED=true (the production default), every business route requires a valid API key. Pass it on each request:

Header Notes
Authorization: Bearer Preferred
X-API-Key: Equivalent alternative

Do not send both conflicting values. Prefer Bearer unless your stack only supports a custom header.

Public routes (no key)

Route Purpose
GET /v1/health-check Liveness
GET /v1/demo/* Limited Vale (VALE3) demo surface

Demo routes are rate-limited by IP, not by API key. Extraction (POST /v1/document-text-extractions) remains a paid, authenticated feature even if you explore demo list endpoints first.

Example: curl with Bearer

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

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

Same call with X-API-Key:

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

Example: Python requests

import os, requests

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

r = requests.get(
    f"{BASE}/v1/companies/resolve",
    params={"query": "PETR4", "by": "ticker"},
    headers=H,
)
r.raise_for_status()
print(r.json()["name"])

Store keys in environment variables or a secret manager. Never commit apicvm_... values to git.

Minimal client wrapper

Centralize the header so every call uses the same credential path:

import os, requests

class Apicvm:
    def __init__(self):
        self.base = os.environ["APICVM_URL"].rstrip("/")
        self.headers = {"Authorization": f"Bearer {os.environ['APICVM_KEY']}"}

    def get(self, path, **params):
        r = requests.get(f"{self.base}{path}", headers=self.headers, params=params)
        if r.status_code == 401:
            raise PermissionError("UNAUTHORIZED — check APICVM_KEY")
        r.raise_for_status()
        return r.json()

api = Apicvm()
company = api.get("/v1/companies/resolve", query="PETR4", by="ticker")
docs = api.get("/v1/documents", ticker="PETR4", type="DFP", year=2024, perPage=10)

The same pattern works in TypeScript, Go, or curl scripts — only the header name changes if you prefer X-API-Key.

Trying the API without a key (demo)

Before you wire production auth, you can explore a narrow public surface:

curl "$APICVM_URL/v1/health-check"
curl "$APICVM_URL/v1/demo/documents?ticker=VALE3&type=FRE&year=2025&perPage=5"

Demo list/download is limited to Vale (VALE3) and is rate-limited per IP. Do not build product features on demo routes — switch to a real key as soon as you need other tickers or text extraction.

What 401 looks like

Missing, invalid, or revoked credentials return 401 with code UNAUTHORIZED:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "...",
    "details": {}
  }
}

Checklist when debugging:

  1. Is the header present on the request that failed?
  2. Did you use Bearer with a space after Bearer?
  3. Is APICVM_KEY the live key (not a truncated copy from chat logs)?
  4. Are you calling a business route (/v1/companies/, /v1/documents/, extractions) rather than assuming demo anonymity?

Auth success does not mean unlimited traffic. After 200 responses, read X-RateLimit-* headers — covered in the errors and rate limits guide.

Auth vs extraction credits

Authentication answers “who is calling.” Extraction credits answer “can this key pay for N PDF pages?” A valid key on the Student plan still receives 403 FORBIDDEN for POST /v1/document-text-extractions. A Pro key with a drained credit balance still gets 202, then a callback with error_code: EXTRACTION_CREDITS_EXCEEDED. Treat those as billing/feature gates, not auth bugs.

Current limitations

  • Auth is required on business routes in production; do not assume anonymous list access.
  • Demo coverage is intentionally narrow (VALE3). Use a paid key for PETR4, ABEV3, and other issuers.
  • Rate limits and extraction credits are separate from authentication — see the errors and rate limits guide.
  • Key provisioning and plan details live in the product signup/billing flow; this guide only covers how to send the key on HTTP requests.

Next steps

Ready to integrate?

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