corvo-nfse
The Python SDK for the Corvo NFS-e API. Register companies, calculate taxes, issue Brazilian service invoices (NFS-e), follow them by webhook and download the PDF and XML, from Python 3.10+.
- Async-first on
httpx, fully typed (py.typed,mypy --strictclean). Library only, no CLI. - Result-style errors. API, network and timeout failures return
(None, error): aCorvoApiErrorcarrying the server's stablecode, the HTTPstatus, therequest_id, field-leveldetailsandretry_after. - Pagination built in. Every list returns
({data, pagination}, None), anditerate()walks all pages for you. - Safe retries. Issuing an invoice and sending an email accept an idempotency key.
- Webhook signature check in constant time.
Full API reference: https://corvo.tech/en/api-docs.
Install
pip install corvo-nfse
Quickstart
Start with a corvo_test_... key: invoices issued with it have no fiscal value.
import asyncio
from corvo_nfse import Corvo
async def main() -> None:
async with Corvo(api_key="corvo_test_...") as corvo: # or set CORVO_API_KEY
page, error = await corvo.companies.list(limit=10)
if error is not None:
# CorvoApiError: code, status, request_id, details, retry_after
print(error.code, error.status, error.request_id, error.details)
else:
print(page["data"], page["pagination"])
asyncio.run(main())
Corvo(...) is the only call that raises: it fails fast with ValueError when no API key is configured or the base URL is invalid.
Result style
Every API method returns (data, error) where exactly one is None:
company, error = await corvo.companies.get("c_1")
if error is not None:
handle(error) # error.code, error.status, error.request_id, error.details
else:
use(company) # the API's JSON, camelCase as the server sends it
A 204 No Content (deletions) returns (None, None).
The one documented exception: the iterate() async generators raise CorvoApiError when a page fails, since a generator has no slot for a tuple.
try:
async for invoice in corvo.invoices.iterate(companyId=company_id):
print(invoice["nfseNumber"])
except CorvoApiError as err:
print(err.code, err.status)
Register a company
company, error = await corvo.companies.create(
{
"legalName": "Empresa Exemplo Ltda",
"taxId": "11222333000181",
"taxRegime": "Simples Nacional", # "MEI" | "Simples Nacional" | "Lucro Presumido" | "Lucro Real"
"municipalityCode": "3550308", # IBGE code, 7 digits
"defaultServiceCode": "010101", # optional: used when an invoice omits service.code
}
)
In live, also upload the A1 certificate (multipart is built for you). The company is ready when company["status"] == "ready" and company["setupGaps"] is empty.
with open("certificado.pfx", "rb") as fh:
cert, error = await corvo.certificates.upload(company["id"], fh.read(), password="...")
Estimate the taxes before issuing
Nothing is issued, stored or charged.
estimate, error = await corvo.invoices.calculate(
{
"companyTaxId": "11222333000181",
"service": {"description": "Consultoria"},
"amount": 1500,
"competenceDate": "2026-10-16",
}
)
# estimate["status"] is "ready" or "blocked": a fiscal gap is data, not an error.
Issue an invoice
Issuing is asynchronous: a successful call means the request was accepted, not that the invoice is authorized. Follow the status with invoices.get() or webhooks.
invoice, error = await corvo.invoices.create(
{
"companyTaxId": "11222333000181", # or "companyId"
# "customer" is optional; when sent, "taxId" (CPF or CNPJ) is required.
"customer": {"taxId": "52998224725", "name": "Cliente", "email": "cliente@example.com"},
"service": {"description": "Consultoria"}, # code and city default to the company's
"amount": 150.5,
"competenceDate": "2026-10-16",
"options": {"email": {"send": True}},
},
idempotency_key="order-42", # the same key never issues twice
)
Use "options": {"type": "SCHEDULE"} with a future competenceDate to schedule the issuance. Request bodies and responses are wire pass-through: camelCase stays camelCase.
Walk every page
async for invoice in corvo.invoices.iterate(companyId=company_id, status="authorized"):
print(invoice["nfseNumber"])
Download the PDF or XML
The invoice must be authorized and have its documents ready.
pdf, error = await corvo.invoices.pdf(invoice["id"])
if pdf is not None:
with open("nfse.pdf", "wb") as fh:
fh.write(pdf.content) # also pdf.mime_type and pdf.file_name
Verify webhook signatures
from corvo_nfse import CorvoWebhooks
async def webhook_handler(request) -> None:
# Use the raw body: re-serialized JSON will never match the HMAC.
signature = request.headers.get("X-Corvo-Signature")
if not CorvoWebhooks.verify_signature(await request.body(), signature, secret):
return # reject with 400
# Deduplicate by the event id: the same event can be delivered more than once.
Webhook events you can subscribe to are exported as corvo_nfse.WEBHOOK_EVENTS. The webhook.test event is only sent by corvo.webhooks.test().
Resources
| Namespace | Operations |
|---|---|
corvo.companies |
create, get, update, remove, list, iterate |
corvo.certificates |
upload, list, remove |
corvo.invoices |
create, calculate, get, list, iterate, events, pdf, xml, download_url, send_email |
corvo.webhooks |
create, remove, test, list, iterate, .deliveries.list, .deliveries.iterate, .deliveries.stats, .deliveries.retry |
corvo.credits |
balance, plans, history, iterate_history |
corvo.billing |
plan, usage |
corvo.plans |
list, get, quote (public price catalog) |
corvo.health |
live, ready |
Changing plans and the billing portal exist only in the dashboard, so they are not in the SDK.
Configuration
| Option | Env fallback | Default |
|---|---|---|
api_key |
CORVO_API_KEY |
required |
base_url |
CORVO_BASE_URL |
https://api.corvo.tech |
prefix |
none | v1 |
timeout |
none | 30.0 seconds |
allow_insecure_http |
none | False (http:// only for loopback and private hosts) |
http_client |
none | an internal httpx.AsyncClient |
Behavior
- Auth. Every request sends
X-API-Key. Keys arecorvo_test_...orcorvo_live_...and belong to one account and environment; the key decides the environment. - Responses. Single resources and list items come back as the API's
data; lists addpagination. - Errors. Network failures, timeouts, non-2xx responses and malformed JSON all return a
CorvoApiError.codeis the API'sERROR_*code, falling back tohttp_<status>; transport failures usenetwork_error,timeoutandinvalid_json. On429,retry_afterholds the seconds to wait.
Development
python -m pip install -e ".[dev]"
python -m pytest # unit tests; the contract test skips without env
python -m mypy src
The contract test in test/contract/ runs only when CORVO_API_KEY and CORVO_BASE_URL point to a reachable instance.
License
MIT, see LICENSE.
Metadata
Release files for corvo-nfse 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| corvo_nfse-0.1.1.tar.gz | 29.5 kB | With provenance | Details for corvo_nfse-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| corvo_nfse-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for corvo_nfse-0.1.1-py3-none-any.whl |
Total release size: 55.4 kB
Release files / corvo_nfse-0.1.1.tar.gz
| Download URL | corvo_nfse-0.1.1.tar.gz |
|---|---|
| Size | 29.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ceed45531721472f2dbaf8a42f759884531394e54df869dce893e78ff267bbb6
|
|
BLAKE2b-256 checksum How to use checksums |
8d831ac8d61f5c957ecbe93d5c0c3a4f1ef33d12a50288f910b83af8afd9e6ad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / corvo_nfse-0.1.1-py3-none-any.whl
| Download URL | corvo_nfse-0.1.1-py3-none-any.whl |
|---|---|
| Size | 26.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6447e7d874c375bb6a74254ed7d7636322d6139b66ba29356437b3da9528e624
|
|
BLAKE2b-256 checksum How to use checksums |
338a0c180702e7dfb373583f4d016a206e62c3a6ac704020fbc7bc1abc994567
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logSecurity for corvo-nfse 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for corvo_nfse-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.