corvo-nfse 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- corvo_nfse-0.1.1/LICENSE +21 -0
- corvo_nfse-0.1.1/PKG-INFO +235 -0
- corvo_nfse-0.1.1/README.md +204 -0
- corvo_nfse-0.1.1/pyproject.toml +54 -0
- corvo_nfse-0.1.1/setup.cfg +4 -0
- corvo_nfse-0.1.1/src/corvo_nfse/__init__.py +90 -0
- corvo_nfse-0.1.1/src/corvo_nfse/client.py +98 -0
- corvo_nfse-0.1.1/src/corvo_nfse/error.py +41 -0
- corvo_nfse-0.1.1/src/corvo_nfse/py.typed +0 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/__init__.py +24 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/billing.py +18 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/certificates.py +43 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/companies.py +45 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/credits.py +42 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/health.py +21 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/invoices.py +147 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/plans.py +31 -0
- corvo_nfse-0.1.1/src/corvo_nfse/resources/webhooks.py +111 -0
- corvo_nfse-0.1.1/src/corvo_nfse/transport.py +335 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/__init__.py +94 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/billing.py +67 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/certificate.py +22 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/common.py +18 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/company.py +88 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/credits.py +85 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/invoice.py +191 -0
- corvo_nfse-0.1.1/src/corvo_nfse/types/webhook.py +81 -0
- corvo_nfse-0.1.1/src/corvo_nfse/webhooks.py +34 -0
- corvo_nfse-0.1.1/src/corvo_nfse.egg-info/PKG-INFO +235 -0
- corvo_nfse-0.1.1/src/corvo_nfse.egg-info/SOURCES.txt +41 -0
- corvo_nfse-0.1.1/src/corvo_nfse.egg-info/dependency_links.txt +1 -0
- corvo_nfse-0.1.1/src/corvo_nfse.egg-info/requires.txt +7 -0
- corvo_nfse-0.1.1/src/corvo_nfse.egg-info/top_level.txt +1 -0
- corvo_nfse-0.1.1/test/test_calculate_plans.py +103 -0
- corvo_nfse-0.1.1/test/test_certificates.py +78 -0
- corvo_nfse-0.1.1/test/test_client.py +94 -0
- corvo_nfse-0.1.1/test/test_companies.py +116 -0
- corvo_nfse-0.1.1/test/test_error.py +46 -0
- corvo_nfse-0.1.1/test/test_invoices.py +236 -0
- corvo_nfse-0.1.1/test/test_smoke.py +6 -0
- corvo_nfse-0.1.1/test/test_transport.py +322 -0
- corvo_nfse-0.1.1/test/test_webhooks_resource.py +299 -0
- corvo_nfse-0.1.1/test/test_webhooks_verify.py +50 -0
corvo_nfse-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corvo Technology
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: corvo-nfse
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for the Corvo NFS-e API: issue Brazilian service invoices, with result-style errors, cursor pagination and webhook verification.
|
|
5
|
+
Author: Corvo Technology
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/corvo-technology/corvo-nfse-python
|
|
8
|
+
Project-URL: Documentation, https://corvo.tech/en/api-docs
|
|
9
|
+
Project-URL: Issues, https://github.com/corvo-technology/corvo-nfse-python/issues
|
|
10
|
+
Project-URL: Repository, https://github.com/corvo-technology/corvo-nfse-python
|
|
11
|
+
Keywords: nfse,nota fiscal,brazil,fiscal,invoice,corvo
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
28
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# corvo-nfse
|
|
33
|
+
|
|
34
|
+
**The Python SDK for the [Corvo](https://corvo.tech) 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+.
|
|
35
|
+
|
|
36
|
+
- **Async-first** on `httpx`, fully typed (`py.typed`, `mypy --strict` clean). Library only, no CLI.
|
|
37
|
+
- **Result-style errors.** API, network and timeout failures return `(None, error)`: a `CorvoApiError` carrying the server's stable `code`, the HTTP `status`, the `request_id`, field-level `details` and `retry_after`.
|
|
38
|
+
- **Pagination built in.** Every list returns `({data, pagination}, None)`, and `iterate()` walks all pages for you.
|
|
39
|
+
- **Safe retries.** Issuing an invoice and sending an email accept an idempotency key.
|
|
40
|
+
- **Webhook signature check** in constant time.
|
|
41
|
+
|
|
42
|
+
Full API reference: <https://corvo.tech/en/api-docs>.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install corvo-nfse
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
Start with a `corvo_test_...` key: invoices issued with it have no fiscal value.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
|
|
57
|
+
from corvo_nfse import Corvo
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def main() -> None:
|
|
61
|
+
async with Corvo(api_key="corvo_test_...") as corvo: # or set CORVO_API_KEY
|
|
62
|
+
page, error = await corvo.companies.list(limit=10)
|
|
63
|
+
if error is not None:
|
|
64
|
+
# CorvoApiError: code, status, request_id, details, retry_after
|
|
65
|
+
print(error.code, error.status, error.request_id, error.details)
|
|
66
|
+
else:
|
|
67
|
+
print(page["data"], page["pagination"])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`Corvo(...)` is the only call that raises: it fails fast with `ValueError` when no API key is configured or the base URL is invalid.
|
|
74
|
+
|
|
75
|
+
## Result style
|
|
76
|
+
|
|
77
|
+
Every API method returns `(data, error)` where exactly one is `None`:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
company, error = await corvo.companies.get("c_1")
|
|
81
|
+
if error is not None:
|
|
82
|
+
handle(error) # error.code, error.status, error.request_id, error.details
|
|
83
|
+
else:
|
|
84
|
+
use(company) # the API's JSON, camelCase as the server sends it
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A `204 No Content` (deletions) returns `(None, None)`.
|
|
88
|
+
|
|
89
|
+
The one documented exception: the `iterate()` async generators raise `CorvoApiError` when a page fails, since a generator has no slot for a tuple.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
try:
|
|
93
|
+
async for invoice in corvo.invoices.iterate(companyId=company_id):
|
|
94
|
+
print(invoice["nfseNumber"])
|
|
95
|
+
except CorvoApiError as err:
|
|
96
|
+
print(err.code, err.status)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Register a company
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
company, error = await corvo.companies.create(
|
|
103
|
+
{
|
|
104
|
+
"legalName": "Empresa Exemplo Ltda",
|
|
105
|
+
"taxId": "11222333000181",
|
|
106
|
+
"taxRegime": "Simples Nacional", # "MEI" | "Simples Nacional" | "Lucro Presumido" | "Lucro Real"
|
|
107
|
+
"municipalityCode": "3550308", # IBGE code, 7 digits
|
|
108
|
+
"defaultServiceCode": "010101", # optional: used when an invoice omits service.code
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
with open("certificado.pfx", "rb") as fh:
|
|
117
|
+
cert, error = await corvo.certificates.upload(company["id"], fh.read(), password="...")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Estimate the taxes before issuing
|
|
121
|
+
|
|
122
|
+
Nothing is issued, stored or charged.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
estimate, error = await corvo.invoices.calculate(
|
|
126
|
+
{
|
|
127
|
+
"companyTaxId": "11222333000181",
|
|
128
|
+
"service": {"description": "Consultoria"},
|
|
129
|
+
"amount": 1500,
|
|
130
|
+
"competenceDate": "2026-10-16",
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
# estimate["status"] is "ready" or "blocked": a fiscal gap is data, not an error.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Issue an invoice
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
invoice, error = await corvo.invoices.create(
|
|
142
|
+
{
|
|
143
|
+
"companyTaxId": "11222333000181", # or "companyId"
|
|
144
|
+
# "customer" is optional; when sent, "taxId" (CPF or CNPJ) is required.
|
|
145
|
+
"customer": {"taxId": "52998224725", "name": "Cliente", "email": "cliente@example.com"},
|
|
146
|
+
"service": {"description": "Consultoria"}, # code and city default to the company's
|
|
147
|
+
"amount": 150.5,
|
|
148
|
+
"competenceDate": "2026-10-16",
|
|
149
|
+
"options": {"email": {"send": True}},
|
|
150
|
+
},
|
|
151
|
+
idempotency_key="order-42", # the same key never issues twice
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use `"options": {"type": "SCHEDULE"}` with a future `competenceDate` to schedule the issuance. Request bodies and responses are wire pass-through: camelCase stays camelCase.
|
|
156
|
+
|
|
157
|
+
## Walk every page
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
async for invoice in corvo.invoices.iterate(companyId=company_id, status="authorized"):
|
|
161
|
+
print(invoice["nfseNumber"])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Download the PDF or XML
|
|
165
|
+
|
|
166
|
+
The invoice must be `authorized` and have its documents ready.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
pdf, error = await corvo.invoices.pdf(invoice["id"])
|
|
170
|
+
if pdf is not None:
|
|
171
|
+
with open("nfse.pdf", "wb") as fh:
|
|
172
|
+
fh.write(pdf.content) # also pdf.mime_type and pdf.file_name
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Verify webhook signatures
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from corvo_nfse import CorvoWebhooks
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def webhook_handler(request) -> None:
|
|
182
|
+
# Use the raw body: re-serialized JSON will never match the HMAC.
|
|
183
|
+
signature = request.headers.get("X-Corvo-Signature")
|
|
184
|
+
if not CorvoWebhooks.verify_signature(await request.body(), signature, secret):
|
|
185
|
+
return # reject with 400
|
|
186
|
+
# Deduplicate by the event id: the same event can be delivered more than once.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Webhook events you can subscribe to are exported as `corvo_nfse.WEBHOOK_EVENTS`. The `webhook.test` event is only sent by `corvo.webhooks.test()`.
|
|
190
|
+
|
|
191
|
+
## Resources
|
|
192
|
+
|
|
193
|
+
| Namespace | Operations |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `corvo.companies` | `create`, `get`, `update`, `remove`, `list`, `iterate` |
|
|
196
|
+
| `corvo.certificates` | `upload`, `list`, `remove` |
|
|
197
|
+
| `corvo.invoices` | `create`, `calculate`, `get`, `list`, `iterate`, `events`, `pdf`, `xml`, `download_url`, `send_email` |
|
|
198
|
+
| `corvo.webhooks` | `create`, `remove`, `test`, `list`, `iterate`, `.deliveries.list`, `.deliveries.iterate`, `.deliveries.stats`, `.deliveries.retry` |
|
|
199
|
+
| `corvo.credits` | `balance`, `plans`, `history`, `iterate_history` |
|
|
200
|
+
| `corvo.billing` | `plan`, `usage` |
|
|
201
|
+
| `corvo.plans` | `list`, `get`, `quote` (public price catalog) |
|
|
202
|
+
| `corvo.health` | `live`, `ready` |
|
|
203
|
+
|
|
204
|
+
Changing plans and the billing portal exist only in the dashboard, so they are not in the SDK.
|
|
205
|
+
|
|
206
|
+
## Configuration
|
|
207
|
+
|
|
208
|
+
| Option | Env fallback | Default |
|
|
209
|
+
| --- | --- | --- |
|
|
210
|
+
| `api_key` | `CORVO_API_KEY` | required |
|
|
211
|
+
| `base_url` | `CORVO_BASE_URL` | `https://api.corvo.tech` |
|
|
212
|
+
| `prefix` | none | `v1` |
|
|
213
|
+
| `timeout` | none | `30.0` seconds |
|
|
214
|
+
| `allow_insecure_http` | none | `False` (`http://` only for loopback and private hosts) |
|
|
215
|
+
| `http_client` | none | an internal `httpx.AsyncClient` |
|
|
216
|
+
|
|
217
|
+
## Behavior
|
|
218
|
+
|
|
219
|
+
- **Auth.** Every request sends `X-API-Key`. Keys are `corvo_test_...` or `corvo_live_...` and belong to one account and environment; the key decides the environment.
|
|
220
|
+
- **Responses.** Single resources and list items come back as the API's `data`; lists add `pagination`.
|
|
221
|
+
- **Errors.** Network failures, timeouts, non-2xx responses and malformed JSON all return a `CorvoApiError`. `code` is the API's `ERROR_*` code, falling back to `http_<status>`; transport failures use `network_error`, `timeout` and `invalid_json`. On `429`, `retry_after` holds the seconds to wait.
|
|
222
|
+
|
|
223
|
+
## Development
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
python -m pip install -e ".[dev]"
|
|
227
|
+
python -m pytest # unit tests; the contract test skips without env
|
|
228
|
+
python -m mypy src
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The contract test in `test/contract/` runs only when `CORVO_API_KEY` and `CORVO_BASE_URL` point to a reachable instance.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# corvo-nfse
|
|
2
|
+
|
|
3
|
+
**The Python SDK for the [Corvo](https://corvo.tech) 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+.
|
|
4
|
+
|
|
5
|
+
- **Async-first** on `httpx`, fully typed (`py.typed`, `mypy --strict` clean). Library only, no CLI.
|
|
6
|
+
- **Result-style errors.** API, network and timeout failures return `(None, error)`: a `CorvoApiError` carrying the server's stable `code`, the HTTP `status`, the `request_id`, field-level `details` and `retry_after`.
|
|
7
|
+
- **Pagination built in.** Every list returns `({data, pagination}, None)`, and `iterate()` walks all pages for you.
|
|
8
|
+
- **Safe retries.** Issuing an invoice and sending an email accept an idempotency key.
|
|
9
|
+
- **Webhook signature check** in constant time.
|
|
10
|
+
|
|
11
|
+
Full API reference: <https://corvo.tech/en/api-docs>.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install corvo-nfse
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
Start with a `corvo_test_...` key: invoices issued with it have no fiscal value.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
|
|
26
|
+
from corvo_nfse import Corvo
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def main() -> None:
|
|
30
|
+
async with Corvo(api_key="corvo_test_...") as corvo: # or set CORVO_API_KEY
|
|
31
|
+
page, error = await corvo.companies.list(limit=10)
|
|
32
|
+
if error is not None:
|
|
33
|
+
# CorvoApiError: code, status, request_id, details, retry_after
|
|
34
|
+
print(error.code, error.status, error.request_id, error.details)
|
|
35
|
+
else:
|
|
36
|
+
print(page["data"], page["pagination"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`Corvo(...)` is the only call that raises: it fails fast with `ValueError` when no API key is configured or the base URL is invalid.
|
|
43
|
+
|
|
44
|
+
## Result style
|
|
45
|
+
|
|
46
|
+
Every API method returns `(data, error)` where exactly one is `None`:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
company, error = await corvo.companies.get("c_1")
|
|
50
|
+
if error is not None:
|
|
51
|
+
handle(error) # error.code, error.status, error.request_id, error.details
|
|
52
|
+
else:
|
|
53
|
+
use(company) # the API's JSON, camelCase as the server sends it
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A `204 No Content` (deletions) returns `(None, None)`.
|
|
57
|
+
|
|
58
|
+
The one documented exception: the `iterate()` async generators raise `CorvoApiError` when a page fails, since a generator has no slot for a tuple.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
try:
|
|
62
|
+
async for invoice in corvo.invoices.iterate(companyId=company_id):
|
|
63
|
+
print(invoice["nfseNumber"])
|
|
64
|
+
except CorvoApiError as err:
|
|
65
|
+
print(err.code, err.status)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Register a company
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
company, error = await corvo.companies.create(
|
|
72
|
+
{
|
|
73
|
+
"legalName": "Empresa Exemplo Ltda",
|
|
74
|
+
"taxId": "11222333000181",
|
|
75
|
+
"taxRegime": "Simples Nacional", # "MEI" | "Simples Nacional" | "Lucro Presumido" | "Lucro Real"
|
|
76
|
+
"municipalityCode": "3550308", # IBGE code, 7 digits
|
|
77
|
+
"defaultServiceCode": "010101", # optional: used when an invoice omits service.code
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
with open("certificado.pfx", "rb") as fh:
|
|
86
|
+
cert, error = await corvo.certificates.upload(company["id"], fh.read(), password="...")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Estimate the taxes before issuing
|
|
90
|
+
|
|
91
|
+
Nothing is issued, stored or charged.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
estimate, error = await corvo.invoices.calculate(
|
|
95
|
+
{
|
|
96
|
+
"companyTaxId": "11222333000181",
|
|
97
|
+
"service": {"description": "Consultoria"},
|
|
98
|
+
"amount": 1500,
|
|
99
|
+
"competenceDate": "2026-10-16",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
# estimate["status"] is "ready" or "blocked": a fiscal gap is data, not an error.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Issue an invoice
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
invoice, error = await corvo.invoices.create(
|
|
111
|
+
{
|
|
112
|
+
"companyTaxId": "11222333000181", # or "companyId"
|
|
113
|
+
# "customer" is optional; when sent, "taxId" (CPF or CNPJ) is required.
|
|
114
|
+
"customer": {"taxId": "52998224725", "name": "Cliente", "email": "cliente@example.com"},
|
|
115
|
+
"service": {"description": "Consultoria"}, # code and city default to the company's
|
|
116
|
+
"amount": 150.5,
|
|
117
|
+
"competenceDate": "2026-10-16",
|
|
118
|
+
"options": {"email": {"send": True}},
|
|
119
|
+
},
|
|
120
|
+
idempotency_key="order-42", # the same key never issues twice
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use `"options": {"type": "SCHEDULE"}` with a future `competenceDate` to schedule the issuance. Request bodies and responses are wire pass-through: camelCase stays camelCase.
|
|
125
|
+
|
|
126
|
+
## Walk every page
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
async for invoice in corvo.invoices.iterate(companyId=company_id, status="authorized"):
|
|
130
|
+
print(invoice["nfseNumber"])
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Download the PDF or XML
|
|
134
|
+
|
|
135
|
+
The invoice must be `authorized` and have its documents ready.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
pdf, error = await corvo.invoices.pdf(invoice["id"])
|
|
139
|
+
if pdf is not None:
|
|
140
|
+
with open("nfse.pdf", "wb") as fh:
|
|
141
|
+
fh.write(pdf.content) # also pdf.mime_type and pdf.file_name
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Verify webhook signatures
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from corvo_nfse import CorvoWebhooks
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def webhook_handler(request) -> None:
|
|
151
|
+
# Use the raw body: re-serialized JSON will never match the HMAC.
|
|
152
|
+
signature = request.headers.get("X-Corvo-Signature")
|
|
153
|
+
if not CorvoWebhooks.verify_signature(await request.body(), signature, secret):
|
|
154
|
+
return # reject with 400
|
|
155
|
+
# Deduplicate by the event id: the same event can be delivered more than once.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Webhook events you can subscribe to are exported as `corvo_nfse.WEBHOOK_EVENTS`. The `webhook.test` event is only sent by `corvo.webhooks.test()`.
|
|
159
|
+
|
|
160
|
+
## Resources
|
|
161
|
+
|
|
162
|
+
| Namespace | Operations |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `corvo.companies` | `create`, `get`, `update`, `remove`, `list`, `iterate` |
|
|
165
|
+
| `corvo.certificates` | `upload`, `list`, `remove` |
|
|
166
|
+
| `corvo.invoices` | `create`, `calculate`, `get`, `list`, `iterate`, `events`, `pdf`, `xml`, `download_url`, `send_email` |
|
|
167
|
+
| `corvo.webhooks` | `create`, `remove`, `test`, `list`, `iterate`, `.deliveries.list`, `.deliveries.iterate`, `.deliveries.stats`, `.deliveries.retry` |
|
|
168
|
+
| `corvo.credits` | `balance`, `plans`, `history`, `iterate_history` |
|
|
169
|
+
| `corvo.billing` | `plan`, `usage` |
|
|
170
|
+
| `corvo.plans` | `list`, `get`, `quote` (public price catalog) |
|
|
171
|
+
| `corvo.health` | `live`, `ready` |
|
|
172
|
+
|
|
173
|
+
Changing plans and the billing portal exist only in the dashboard, so they are not in the SDK.
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
| Option | Env fallback | Default |
|
|
178
|
+
| --- | --- | --- |
|
|
179
|
+
| `api_key` | `CORVO_API_KEY` | required |
|
|
180
|
+
| `base_url` | `CORVO_BASE_URL` | `https://api.corvo.tech` |
|
|
181
|
+
| `prefix` | none | `v1` |
|
|
182
|
+
| `timeout` | none | `30.0` seconds |
|
|
183
|
+
| `allow_insecure_http` | none | `False` (`http://` only for loopback and private hosts) |
|
|
184
|
+
| `http_client` | none | an internal `httpx.AsyncClient` |
|
|
185
|
+
|
|
186
|
+
## Behavior
|
|
187
|
+
|
|
188
|
+
- **Auth.** Every request sends `X-API-Key`. Keys are `corvo_test_...` or `corvo_live_...` and belong to one account and environment; the key decides the environment.
|
|
189
|
+
- **Responses.** Single resources and list items come back as the API's `data`; lists add `pagination`.
|
|
190
|
+
- **Errors.** Network failures, timeouts, non-2xx responses and malformed JSON all return a `CorvoApiError`. `code` is the API's `ERROR_*` code, falling back to `http_<status>`; transport failures use `network_error`, `timeout` and `invalid_json`. On `429`, `retry_after` holds the seconds to wait.
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
python -m pip install -e ".[dev]"
|
|
196
|
+
python -m pytest # unit tests; the contract test skips without env
|
|
197
|
+
python -m mypy src
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The contract test in `test/contract/` runs only when `CORVO_API_KEY` and `CORVO_BASE_URL` point to a reachable instance.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "corvo-nfse"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Python SDK for the Corvo NFS-e API: issue Brazilian service invoices, with result-style errors, cursor pagination and webhook verification."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Corvo Technology" }]
|
|
13
|
+
keywords = ["nfse", "nota fiscal", "brazil", "fiscal", "invoice", "corvo"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["httpx>=0.27"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/corvo-technology/corvo-nfse-python"
|
|
29
|
+
Documentation = "https://corvo.tech/en/api-docs"
|
|
30
|
+
Issues = "https://github.com/corvo-technology/corvo-nfse-python/issues"
|
|
31
|
+
Repository = "https://github.com/corvo-technology/corvo-nfse-python"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8",
|
|
36
|
+
"pytest-asyncio>=0.23",
|
|
37
|
+
"respx>=0.21",
|
|
38
|
+
"mypy>=1.10",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
corvo_nfse = ["py.typed"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["test"]
|
|
49
|
+
asyncio_mode = "auto"
|
|
50
|
+
addopts = "-q"
|
|
51
|
+
|
|
52
|
+
[tool.mypy]
|
|
53
|
+
strict = true
|
|
54
|
+
python_version = "3.10"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""corvo-nfse — the Python SDK for the Corvo NFS-e API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .client import Corvo
|
|
6
|
+
from .error import CorvoApiError, CorvoErrorDetail, Result
|
|
7
|
+
from .transport import DownloadedFile, Query, Unwrap
|
|
8
|
+
from .types import (
|
|
9
|
+
WEBHOOK_EVENTS,
|
|
10
|
+
BillingPlanResponse,
|
|
11
|
+
Certificate,
|
|
12
|
+
Company,
|
|
13
|
+
CompanyAddressInput,
|
|
14
|
+
CompanyCreateParams,
|
|
15
|
+
CompanyUpdateParams,
|
|
16
|
+
CreditBalance,
|
|
17
|
+
CreditPlan,
|
|
18
|
+
CreditTransaction,
|
|
19
|
+
Invoice,
|
|
20
|
+
InvoiceCalculation,
|
|
21
|
+
InvoiceCreateParams,
|
|
22
|
+
InvoiceCustomerInput,
|
|
23
|
+
InvoiceDownloadUrl,
|
|
24
|
+
InvoiceEmailResult,
|
|
25
|
+
InvoiceEvent,
|
|
26
|
+
InvoiceListParams,
|
|
27
|
+
InvoiceStage,
|
|
28
|
+
InvoiceStatus,
|
|
29
|
+
Paginated,
|
|
30
|
+
Pagination,
|
|
31
|
+
PlanQuote,
|
|
32
|
+
PlanRecommendation,
|
|
33
|
+
QuotaStatus,
|
|
34
|
+
TaxRegime,
|
|
35
|
+
Webhook,
|
|
36
|
+
WebhookCreateParams,
|
|
37
|
+
WebhookDelivery,
|
|
38
|
+
WebhookDeliveryStats,
|
|
39
|
+
WebhookEvent,
|
|
40
|
+
WebhookWithSecret,
|
|
41
|
+
)
|
|
42
|
+
from .webhooks import CorvoWebhooks
|
|
43
|
+
|
|
44
|
+
VERSION = "0.1.1"
|
|
45
|
+
__version__ = VERSION
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"VERSION",
|
|
49
|
+
"__version__",
|
|
50
|
+
"WEBHOOK_EVENTS",
|
|
51
|
+
"BillingPlanResponse",
|
|
52
|
+
"Certificate",
|
|
53
|
+
"Company",
|
|
54
|
+
"CompanyAddressInput",
|
|
55
|
+
"CompanyCreateParams",
|
|
56
|
+
"CompanyUpdateParams",
|
|
57
|
+
"Corvo",
|
|
58
|
+
"CorvoApiError",
|
|
59
|
+
"CorvoErrorDetail",
|
|
60
|
+
"CorvoWebhooks",
|
|
61
|
+
"CreditBalance",
|
|
62
|
+
"CreditPlan",
|
|
63
|
+
"CreditTransaction",
|
|
64
|
+
"DownloadedFile",
|
|
65
|
+
"Invoice",
|
|
66
|
+
"InvoiceCalculation",
|
|
67
|
+
"InvoiceCreateParams",
|
|
68
|
+
"InvoiceCustomerInput",
|
|
69
|
+
"InvoiceDownloadUrl",
|
|
70
|
+
"InvoiceEmailResult",
|
|
71
|
+
"InvoiceEvent",
|
|
72
|
+
"InvoiceListParams",
|
|
73
|
+
"InvoiceStage",
|
|
74
|
+
"InvoiceStatus",
|
|
75
|
+
"Paginated",
|
|
76
|
+
"Pagination",
|
|
77
|
+
"PlanQuote",
|
|
78
|
+
"PlanRecommendation",
|
|
79
|
+
"Query",
|
|
80
|
+
"QuotaStatus",
|
|
81
|
+
"Result",
|
|
82
|
+
"TaxRegime",
|
|
83
|
+
"Unwrap",
|
|
84
|
+
"Webhook",
|
|
85
|
+
"WebhookCreateParams",
|
|
86
|
+
"WebhookDelivery",
|
|
87
|
+
"WebhookDeliveryStats",
|
|
88
|
+
"WebhookEvent",
|
|
89
|
+
"WebhookWithSecret",
|
|
90
|
+
]
|