corvo-nfse

Elixir 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 Elixir 1.16+.

Full API reference: https://corvo.tech/en/api-docs.

Installation

def deps do
  [
    {:corvo_nfse, "~> 0.1.1"}
  ]
end

Quickstart

Start with a corvo_test_... key: invoices issued with it have no fiscal value.

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

{: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.

{: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.

{: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

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

{: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.

{: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

[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

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.