Async CVM PDF Text Extraction with Webhook Callbacks
Brazilian CVM filings are PDFs — often hundreds of pages in Portuguese. For LLM pipelines you need structured text, not raw bytes. apicvm offers CVM PDF extraction via webhook: enqueue a job, receive one callback per page with markdown, and assemble the document in your app.
This guide covers the async contract, callback payloads, security constraints, and a minimal receiver pattern.
The problem
Synchronous PDF-to-text over HTTP does not scale for large DFP or FRE files:
- Multi-hundred-page PDFs exceed reasonable HTTP timeouts
- Page-level progress matters for streaming into RAG indexes
- Retries and partial failure need a durable pattern
apicvm handles extraction in a background worker. Progress arrives only through callbacks — there is no HTTP job-status endpoint.
The async flow
1. GET /v1/documents?ticker=PETR4&type=DFP&year=2024 → pick document.id
2. POST /v1/document-text-extractions → 202 { id, status: "queued", document_id }
3. Page workers process pages in parallel → POST callback_url per page (status: success)
4. Parent job finishes → POST callback_url with status: completed
Step 1: Enqueue extraction
export APICVM_KEY='apicvm_...'
export APICVM_URL='https://apicvm.dev'
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": "<document-uuid>" }
}' \
"$APICVM_URL/v1/document-text-extractions"
Response 202:
{
"id": "42",
"status": "queued",
"document_id": "550e8400-e29b-41d4-a716-446655440000"
}
Note: job response fields use snake_case (document_id), unlike most of the API.
If a page already has cached ocr_text, that page job reuses it (credits are still debited at enqueue for the full page count). Pages may arrive out of order.
Step 2: Handle page callbacks
Your endpoint receives one POST per page (up to 3 retries with backoff). Do not treat page order as completion — wait for status: "completed".
Success payload (per page):
{
"status": "success",
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"job_id": "42",
"page": {
"number": 1,
"markdown": "# Section title\n\nParagraph text..."
},
"total_pages": 120
}
Completed payload (document ready):
{
"status": "completed",
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"job_id": "42",
"total_pages": 120
}
Error payload:
{
"status": "error",
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"job_id": "42",
"error_message": "Description of the error"
}
Design your handler to be idempotent — the worker may retry callbacks.
Example: minimal Python receiver (Flask)
from flask import Flask, request
app = Flask(__name__)
pages = {}
@app.post("/callbacks/apicvm")
def apicvm_callback():
body = request.json
doc_id = body["document_id"]
if body["status"] == "error":
print(f"Job failed: {body['error_message']}")
return "", 200
if body["status"] == "completed":
ordered = [pages[doc_id][i] for i in sorted(pages.get(doc_id, {}))]
full_text = "\n\n".join(ordered)
# chunk → embed → index
print(f"Done: {doc_id}, {len(ordered)} pages")
return "", 200
page_num = body["page"]["number"]
pages.setdefault(doc_id, {})[page_num] = body["page"]["markdown"]
return "", 200
Run behind HTTPS in production. Local development may allow http:// when the server has APICVM_CALLBACK_ALLOW_HTTP=true — production requires HTTPS.
Callback URL restrictions
In staging and production:
| Rule | Detail |
|---|---|
| HTTPS required | http:// blocked unless explicitly allowed in dev |
| No localhost | localhost and *.localhost blocked |
| No private IPs | RFC1918, loopback, link-local, cloud metadata blocked |
| DNS resolution | All resolved IPs checked before POST |
| No embedded credentials | URLs with user:pass rejected |
Invalid URLs return 422 INVALID_CALLBACK_URL with a reason code (PRIVATE_IP, LOCALHOST, etc.).
Wiring into a RAG pipeline
Typical agent loop:
- Resolve company → list DFP/ITR → pick filing
- POST extraction with your agent server's callback URL
- On each page callback, chunk markdown and upsert to vector store
- On
status: "completed", mark document ready for queries
See also: Build a RAG Pipeline with Brazil CVM Filings.
Current limitations
- No job status endpoint — track progress only via callbacks or your own state.
- Callback timeout is 30 seconds per POST; keep handlers fast.
- Extraction quality depends on PDF layout; complex tables may need post-processing.
- Corpus coverage depends on the Hold ingestion pipeline — not every filing is available immediately after CVM publication.
Next steps
Ready to integrate?
Get an API key and start querying Brazilian CVM filings programmatically.