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).

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.