How to Extract Text from Brazilian CVM PDFs for AI Agents
LLMs and RAG systems need text, not binary PDFs. CVM filings — DFP, ITR, FRE — are often scanned tables, multi-column layouts, and Portuguese financial notes that break naive pdftotext pipelines. If you want to extract text from CVM filings reliably for AI agents, you need page-level markdown with structure preserved.
apicvm enqueues async extraction jobs and delivers one callback per page with markdown content. This guide covers the full flow: list a document, start extraction, handle callbacks, and wire it into an agent or RAG pipeline.
The problem
Building on Brazilian regulatory PDFs hits predictable walls:
- Layout complexity — financial tables span pages; simple text extraction loses structure.
- File size — DFP and FRE documents can exceed hundreds of pages; loading entire PDFs into LLM context is impractical.
- Pipeline fragility — scraping CVM portals and running local OCR breaks when formats change.
You need a service that returns page-by-page markdown with a stable API contract.
How apicvm extraction works
Extraction is asynchronous — there is no synchronous "give me all text" endpoint.
1. GET /v1/documents → find document.id
2. POST /v1/document-text-extractions → enqueue job (202)
3. Your callback URL → receives one POST per page with markdown
Each successful callback includes:
{
"status": "success",
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"job_id": "42",
"page": {
"number": 1,
"markdown": "# Demonstrações Financeiras\n\n..."
},
"current_page": 1,
"total_pages": 120,
"is_last_page": false
}
Progress is only available through callbacks — no HTTP job status endpoint exists in v1.
Step 1: Get a document ID
export APICVM_KEY='apicvm_...'
export APICVM_URL='https://apicvm.dev'
curl -H "Authorization: Bearer $APICVM_KEY" \
"$APICVM_URL/v1/documents?ticker=PETR4&type=DFP&year=2024&perPage=5"
Copy the id UUID from the document you want to extract.
Step 2: Enqueue extraction
Your callback URL must be HTTPS in production (private IPs and localhost are blocked).
curl -X POST -H "Authorization: Bearer $APICVM_KEY" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://your-app.example.com/callbacks/apicvm",
"document": {
"id": "550e8400-e29b-41d4-a716-446655440000"
}
}' \
"$APICVM_URL/v1/document-text-extractions"
Response (202):
{
"id": "42",
"status": "queued",
"document_id": "550e8400-e29b-41d4-a716-446655440000"
}
If the document was previously extracted with all pages done, the worker may reuse cached OCR text.
Step 3: Handle callbacks
Your endpoint must be idempotent — the worker retries failed callbacks up to 3 times.
Minimal FastAPI handler:
from fastapi import FastAPI, Request
from collections import defaultdict
app = FastAPI()
pages: dict[str, dict[int, str]] = defaultdict(dict)
@app.post("/callbacks/apicvm")
async def apicvm_callback(request: Request):
payload = await request.json()
status = payload.get("status")
if status == "error":
print(f"Extraction error: {payload.get('error_message')}")
return {"ok": True}
if status == "success":
doc_id = payload["document_id"]
page_num = payload["page"]["number"]
markdown = payload["page"]["markdown"]
pages[doc_id][page_num] = markdown
print(
f"Doc {doc_id}: page {payload['current_page']}/{payload['total_pages']}"
)
if payload.get("is_last_page"):
full_text = "\n\n".join(
pages[doc_id][n] for n in sorted(pages[doc_id])
)
# Feed into vector store, agent memory, etc.
print(f"Extraction complete — {len(pages[doc_id])} pages")
del pages[doc_id]
return {"ok": True}
Python: enqueue from a script
import os
import requests
BASE = os.environ["APICVM_URL"]
H = {
"Authorization": f"Bearer {os.environ['APICVM_KEY']}",
"Content-Type": "application/json",
}
def enqueue_extraction(document_id: str, callback_url: str) -> dict:
r = requests.post(
f"{BASE}/v1/document-text-extractions",
headers=H,
json={"callback_url": callback_url, "document": {"id": document_id}},
)
r.raise_for_status()
return r.json()
# List → extract workflow
docs = requests.get(
f"{BASE}/v1/documents",
headers={"Authorization": H["Authorization"]},
params={"ticker": "PETR4", "type": "DFP", "year": 2024, "perPage": 1},
).json()
doc_id = docs["data"][0]["id"]
job = enqueue_extraction(doc_id, "https://your-app.example.com/callbacks/apicvm")
print(f"Job {job['id']} queued for document {job['document_id']}")
Building a RAG pipeline
Typical architecture for Brazil filings AI agents:
apicvm list documents
↓
POST document-text-extractions
↓
callback handler receives pages
↓
chunk markdown (by heading or token limit)
↓
embed → vector store (Pinecone, pgvector, etc.)
↓
agent retrieves chunks + cites page number
Chunk by markdown headings (#, ##) to keep financial tables intact where possible. Store metadata: ticker, type, year, page.number, document_id.
For LLM discovery of apicvm capabilities, see /llms.txt.
Error callbacks
On failure:
{
"status": "error",
"document_id": "...",
"job_id": "42",
"error_message": "Description of the error"
}
Log and alert on these; re-enqueue if transient.
Current limitations
- Async only — no synchronous extraction or polling endpoint in v1.
- Callback required — you must expose a reachable HTTPS URL in production.
- One document per job — specify
document.idexplicitly; ambiguous filters are rejected. - Portuguese source text — markdown reflects the original filing language.
- Corpus coverage — extraction works on documents in the apicvm database with files in the bucket.
- Rate limit — 1 document per API key per UTC day for
POST /v1/document-text-extractions(each request counts, including cache hits). Other endpoints use the default per-minute limit.
Next steps
- Read the API docs — callback schema, SSRF rules, error codes
- Get an API key
- Download llms.txt — machine-readable API summary for LLM agents
- Access CVM filings with Python — resolve and list documents
Ready to integrate?
Get an API key and start querying Brazilian CVM filings programmatically.