Packages
corvo_nfse
0.1.1
Elixir SDK for the Corvo NFS-e API: issue Brazilian service invoices, calculate taxes, cursor pagination, result-style errors and webhook signature verification.
Current section
Files
Jump to
Current section
Files
corvo_nfse
README.md
README.md
# corvo-nfse
Elixir 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 Elixir 1.16+.
- **Result-style, never raises.** Every API, network or timeout failure comes back as `{:error, %CorvoNfse.Error{}}` with `status`, `code`, `message`, `details`, `request_id` and `retry_after`.
- **Pagination built in.** `list/2` returns one page; `iterate/2` returns a `Stream` that follows `hasMore` and `nextCursor`.
- **Safe retries.** Issuing an invoice and sending an email accept an idempotency key.
- **Webhook verification in one call.** HMAC-SHA256 against the raw body, constant-time.
- The wire format is passed through unchanged: responses are plain maps with string keys, exactly as the server sent them. Library only, no CLI.
Full API reference: <https://corvo.tech/en/api-docs>.
## Installation
```elixir
def deps do
[
{:corvo_nfse, "~> 0.1.1"}
]
end
```
## Quickstart
Start with a `corvo_test_...` key: invoices issued with it have no fiscal value.
```elixir
client = CorvoNfse.new(api_key: "corvo_test_...") # or set CORVO_API_KEY
{:ok, page} = CorvoNfse.Resources.Companies.list(client, limit: 10)
page.data #=> [%{"id" => "...", "legalName" => "..."}]
page.pagination #=> %{"limit" => 10, "hasMore" => true, "nextCursor" => "eyJ..."}
```
`CorvoNfse.new/1` is the only call that raises (`ArgumentError`): it fails fast when no API key is configured or the base URL is invalid.
### Register a company
```elixir
{:ok, company} =
CorvoNfse.Resources.Companies.create(client, %{
"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 with `CorvoNfse.Resources.Certificates.upload/4`. The company is ready when `company["status"] == "ready"` and `company["setupGaps"] == []`.
### Estimate the taxes before issuing
Nothing is issued, stored or charged.
```elixir
{:ok, estimate} =
CorvoNfse.Resources.Invoices.calculate(client, %{
"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/2` or webhooks.
```elixir
{:ok, invoice} =
CorvoNfse.Resources.Invoices.create(
client,
%{
"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.
### Handling errors
```elixir
case CorvoNfse.Resources.Companies.get(client, "comp_missing") do
{:ok, company} ->
company
{:error, %CorvoNfse.Error{status: 404, code: code, request_id: request_id}} ->
Logger.warning("not found code=#{code} request_id=#{request_id}")
end
```
`CorvoNfse.Error` fields: `status`, `code` (the API's `ERROR_*` code, or `http_<status>`, `network_error`, `timeout`, `invalid_json`), `message`, `details`, `request_id`, `retry_after` (seconds, on `429`).
### Pagination and streaming
```elixir
{:ok, stream} = CorvoNfse.Resources.Invoices.iterate(client, status: "authorized", limit: 50)
stream
|> Stream.take(200)
|> Enum.each(&index/1)
```
`iterate` fetches the first page eagerly: a failure there returns `{:error, %CorvoNfse.Error{}}` and nothing was streamed. If a page after the first fails while you consume the stream, the stream raises the `%CorvoNfse.Error{}`.
### Download the PDF or XML
The invoice must be `authorized` and have its documents ready.
```elixir
{:ok, %{body: pdf, mime_type: "application/pdf", file_name: file_name}} =
CorvoNfse.Resources.Invoices.pdf(client, "inv_1")
File.write!(file_name, pdf)
```
### Verify webhooks
```elixir
[signature] = Plug.Conn.get_req_header(conn, "x-corvo-signature")
{:ok, raw_body, _conn} = Plug.Conn.read_body(conn) # the raw bytes, not the parsed JSON
if CorvoNfse.Webhooks.verify_signature(raw_body, signature, secret) do
# verified; deduplicate by the event id, the same event can arrive twice
end
```
`CorvoNfse.Types.Webhook.events/0` lists the 9 events you can subscribe to. The `webhook.test` event is only sent by `Webhooks.test/2`.
## Resources
| Resource | Operations |
|---|---|
| `CorvoNfse.Resources.Companies` | `create`, `list`, `iterate`, `get`, `update`, `remove` |
| `CorvoNfse.Resources.Certificates` | `upload`, `list`, `remove` |
| `CorvoNfse.Resources.Invoices` | `create`, `calculate`, `get`, `list`, `iterate`, `events`, `pdf`, `xml`, `download_url`, `send_email` |
| `CorvoNfse.Resources.Webhooks` | `list`, `create`, `remove`, `test` |
| `CorvoNfse.Resources.Webhooks.Deliveries` | `list`, `iterate`, `stats`, `retry` |
| `CorvoNfse.Resources.Credits` | `balance`, `plans`, `history`, `iterate_history` |
| `CorvoNfse.Resources.Billing` | `plan`, `usage` |
| `CorvoNfse.Resources.Plans` | `list`, `get`, `quote` (public price catalog) |
| `CorvoNfse.Resources.Health` | `live`, `ready` |
Changing plans and the billing portal exist only in the dashboard, so they are not in the SDK. `CorvoNfse.Types.*` documents every wire type with `defstruct` and `@type`.
## Configuration
`CorvoNfse.new/1` options (with environment fallbacks):
| Option | Environment | Default |
|---|---|---|
| `:api_key` | `CORVO_API_KEY` | required |
| `:base_url` | `CORVO_BASE_URL` | `https://api.corvo.tech` |
| `:prefix` | `CORVO_API_PREFIX` | `"v1"` |
| `:receive_timeout` | none | 30_000 ms |
| `:allow_insecure_http` | none | `false` (`http://` only for loopback and private hosts) |
Auth is the `X-API-Key` header. Keys are `corvo_test_...` or `corvo_live_...` and belong to one account and environment; the key decides the environment.
## Development
```bash
mix deps.get
mix test # the contract test is excluded without env
mix credo --strict
```
The contract test runs only when `CORVO_API_KEY` and `CORVO_BASE_URL` point to a reachable instance.
## License
MIT, see [LICENSE](LICENSE).