Handle Errors and Rate Limits on the CVM Filings API
Production clients need a clear model for the CVM API rate limit, auth failures, missing documents, and extraction credit gates. apicvm returns a uniform error envelope and standard X-RateLimit-* headers so you can retry safely without guessing.
The problem
Pipelines that ignore 429, treat every 404 the same, or enqueue extractions without checking credits fail in non-obvious ways. You want: one JSON shape, documented codes, and explicit rate-limit headers.
Error envelope
All error responses follow:
{
"error": {
"code": "DOCUMENT_NOT_FOUND",
"message": "Documento não encontrado",
"details": {}
}
}
Branch on error.code (stable) rather than free-text message (may be localized).
Status codes you should handle
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED |
Missing, invalid, or revoked API key |
| 403 | FORBIDDEN |
Feature unavailable on plan (e.g. markdown extraction on Student) |
| 404 | COMPANY_NOT_FOUND |
Resolve matched nothing |
| 404 | DOCUMENT_NOT_FOUND |
Document or file object missing |
| 409 | AMBIGUOUS_RESULT |
Resolve returned multiple candidates |
| 422 | validation / INVALID_CALLBACK_URL |
Bad params or blocked callback URL |
| 429 | RATE_LIMIT_EXCEEDED |
Key exceeded its window (limit, remaining, resetAt) |
Rate limits
Authenticated routes are limited per API key. Student and Pro plans use a global window of 60 requests / 60 seconds (see current plan table in the API contract). Student also has a daily cap; Pro extraction uses page credits, not an extra daily extract ceiling.
Response headers on authenticated routes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Max requests in the current window |
X-RateLimit-Remaining |
Remaining in the window |
X-RateLimit-Reset |
Unix timestamp when the window resets |
Example: detect 429 in Python
import os, time, requests
BASE = os.environ["APICVM_URL"]
H = {"Authorization": f"Bearer {os.environ['APICVM_KEY']}"}
def get(path, **params):
while True:
r = requests.get(f"{BASE}{path}", headers=H, params=params)
if r.status_code != 429:
r.raise_for_status()
return r.json()
body = r.json()["error"]
reset_at = body.get("details", {}).get("resetAt")
sleep_for = max(1, int(reset_at) - int(time.time())) if reset_at else 5
time.sleep(min(sleep_for, 60))
docs = get("/v1/documents", ticker="PETR4", type="DFP", year=2024, perPage=20)
Prefer reading X-RateLimit-Remaining before tight loops (watchlists, multi-ticker jobs).
Extraction credits vs request limits
POST /v1/document-text-extractions returns 202 immediately and is gated by credits in the prepare job (1 credit = 1 PDF page). Insufficient balance sends a callback with error_code: EXTRACTION_CREDITS_EXCEEDED (balance, required) — nothing is processed and nothing is debited on a failed gate.
File download (GET /v1/documents/:id/file) does not consume extraction credits.
Distinguish failure classes in logs
| Class | Codes | Client action |
|---|---|---|
| Auth | 401 |
Fix key; do not retry blindly |
| Plan / credits | 402, 403 |
Upgrade plan or buy credits; surface to operator |
| Not found | 404 |
Remap ticker/year; may be corpus lag |
| Ambiguous resolve | 409 |
Disambiguate with CNPJ or pick from candidates |
| Validation | 422 |
Fix params (perPage, callback URL, …) |
| Throttle | 429 |
Sleep until resetAt; reduce concurrency |
Ambiguous company resolve
{
"error": {
"code": "AMBIGUOUS_RESULT",
"message": "Mais de uma empresa corresponde à busca",
"details": { "candidates": [/* companies */] }
}
}
Retry with by=cnpj or a more specific ticker rather than looping the same name query.
Invalid extraction callback
Production callback_url values must be HTTPS and pass SSRF checks. Failures return 422 / INVALID_CALLBACK_URL — fix the URL before re-enqueueing, or you will burn operator time without consuming credits (the job never starts).
Recommended retry policy
- Never retry
401,403, or422without changing inputs. - Retry
429only after waiting forresetAt(cap sleep, e.g. 60s). - Retry
5xxwith exponential backoff and jitter; logerror.code. - For extractions, treat callback delivery as the source of truth — there is no status polling endpoint.
Current limitations
- There is no HTTP job-status endpoint for extractions — progress arrives only via callback.
- Plan limits can change; treat the contract and billing portal as source of truth for quotas.
- Demo routes use a separate per-IP limit — do not mix demo and paid traffic assumptions.
- Cache hits on extraction still debit page credits — a “free replay” assumption will undercount cost.
Next steps
Ready to integrate?
Get an API key and start querying Brazilian CVM filings programmatically.