Access Brazilian CVM Filings with TypeScript
Node.js backends, Next.js API routes, and serverless functions often need Brazil CVM filings in TypeScript — not Python scripts. apicvm exposes a JSON REST API that works cleanly with fetch, typed responses, and standard error handling.
This guide shows a minimal TypeScript client: resolve a company, list documents, download a PDF, and enqueue async text extraction.
The problem
Brazilian regulatory data tooling skews toward Python (pandas, Jupyter). TypeScript teams building fintech apps, internal dashboards, or agent backends still need:
- Typed company and document models
- Pagination over large result sets
- Stream handling for PDF downloads
- Structured API errors (
409 AMBIGUOUS_RESULT,429 RATE_LIMIT_EXCEEDED)
You should not need a Python sidecar for basic filings access.
Setup
export APICVM_KEY='apicvm_...'
export APICVM_URL='https://apicvm.dev'
All business routes require the API key:
const baseUrl = process.env.APICVM_URL!
const apiKey = process.env.APICVM_KEY!
const headers = {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
}
Types (minimal)
type Ticker = { id: number; idCompany: number; ticker: string; tickerClass: string }
type Company = {
id: number
name: string
cnpj: string
sector: string | null
tickers: Ticker[]
}
type Document = {
id: string
type: string
dateRef: string | null
idCompany: number
year: number
name: string | null
hash: string | null
company: Company
}
type Paginated<T> = {
meta: {
total: number
perPage: number
currentPage: number
lastPage: number
}
data: T[]
}
type ApiError = {
error: { code: string; message: string; details: Record<string, unknown> }
}
Resolve a company
async function resolveCompany(query: string, by = 'auto'): Promise<Company> {
const url = new URL(`${baseUrl}/v1/companies/resolve`)
url.searchParams.set('query', query)
url.searchParams.set('by', by)
const res = await fetch(url, { headers })
if (!res.ok) {
const err = (await res.json()) as ApiError
throw new Error(`${err.error.code}: ${err.error.message}`)
}
return res.json() as Promise<Company>
}
const petrobras = await resolveCompany('PETR4', 'ticker')
console.log(petrobras.name, petrobras.cnpj)
List documents
async function listDocuments(params: Record<string, string>): Promise<Paginated<Document>> {
const url = new URL(`${baseUrl}/v1/documents`)
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
const res = await fetch(url, { headers })
if (!res.ok) throw new Error(`List failed: ${res.status}`)
return res.json() as Promise<Paginated<Document>>
}
const dfp2024 = await listDocuments({
ticker: 'PETR4',
type: 'DFP',
year: '2024',
perPage: '20',
})
for (const doc of dfp2024.data) {
console.log(doc.id, doc.name)
}
Paginate with page and perPage (max 50). Default document sort: year desc, dateRef desc, name asc.
Download a PDF
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
async function downloadDocument(documentId: string, destPath: string) {
const res = await fetch(`${baseUrl}/v1/documents/${documentId}/file`, { headers })
if (!res.ok) throw new Error(`Download failed: ${res.status}`)
if (!res.body) throw new Error('No response body')
await pipeline(res.body as NodeJS.ReadableStream, createWriteStream(destPath))
}
Response headers include Content-Type (application/pdf), Content-Length, and Content-Disposition.
Enqueue text extraction
async function enqueueExtraction(documentId: string, callbackUrl: string) {
const res = await fetch(`${baseUrl}/v1/document-text-extractions`, {
method: 'POST',
headers,
body: JSON.stringify({
callback_url: callbackUrl,
document: { id: documentId },
}),
})
if (res.status !== 202) throw new Error(`Enqueue failed: ${res.status}`)
return res.json() as Promise<{ id: string; status: string; document_id: string }>
}
Extraction is async — handle page callbacks on your HTTPS endpoint. See Async CVM PDF Extraction with Webhook Callbacks.
Rate limits
Check response headers on authenticated routes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Max requests per window |
X-RateLimit-Remaining |
Requests left |
X-RateLimit-Reset |
Window reset (Unix timestamp) |
On 429, back off until resetAt in the error details.
curl equivalent
curl -H "Authorization: Bearer $APICVM_KEY" \
"$APICVM_URL/v1/companies/resolve?query=PETR4&by=ticker"
The CONTRACT.md file includes a full TypeScript snippet you can copy into your project.
Current limitations
- No official npm SDK yet — thin
fetchwrapper is sufficient for v1. - Job progress for extractions is callback-only (no polling endpoint).
- Corpus reflects ingested filings; not real-time relative to CVM publication.
Next steps
Ready to integrate?
Get an API key and start querying Brazilian CVM filings programmatically.