Swagger UI openapi.json openapi.yaml
Guide

Invoice Emission

The actual NFS-e emission flow: invoice model, lifecycle, async emission pipeline, cancellation, safety-net worker, and callbacks. Assumes the fiscal entity is already set up (see Invoice Emission Setup).

TL;DR

Invoices are async fire-and-forget. POST /invoices returns pending immediately; the final state comes via a callback or a safety-net worker.
1

Create

POST /fiscal-entities/{id}/invoices — the invoice is persisted with status: pending and emission happens asynchronously.

2

Emit (async)

The system builds the NFS-e request, calls the provider (eNotas), and sets the status to requested.

3

Resolve

The provider sends a callback with the final status, or a safety-net worker polls every 2 min as fallback. The status moves to succeeded or failed.

Done

The invoice's data and urls are populated with the provider-issued number and PDF/XML download links.

Endpoints

MethodPathPurpose
GET/fiscal-entities/:id/invoicesList invoices for the fiscal entity. Filters: tenant_id (or seller_id from foreign_id), pagination.
GET/fiscal-entities/:id/invoices/:invoice_idGet a single invoice. 404 if not found.
POST/fiscal-entities/:id/invoicesCreate & emit a new invoice. Persists as pending, emission happens asynchronously.
POST/fiscal-entities/:id/invoices/:invoice_id/cancelBegin the cancellation flow.
POST/fiscal-entities/:id/invoices/:invoice_id/emitRe-try emission on a failed invoice.

The Invoice model

Table: accounting_invoices. One row per NFS-e request.

Core fields

  • Typeservices | products.
  • Status — see the lifecycle below.
  • SellerId — the SellerFiscalEntity id.
  • BuyerId — optional BuyerFiscalEntity id.
  • FiscalDate"YYYY-MM-DD".
  • Amount — in cents (integer). 1000 = R$ 10,00.
  • ItemsSourcecheckout | bill | manual.

Collections

  • Items[] — line items (description, quantity, unit price, total).
  • Taxes[] — tax items with percentages.
  • Partners[] — commission splits (for marketplace flows).

Sub-documents

  • Emissions — JSONB column, one per attempt. Each entry has status, provider, providerId, providerErrors, providerStatuses (history), and providerRequest (the full payload sent).
  • Data — provider-issued data: number, code, emission_date, cancelation_date.
  • Urlspdf and xml download links.
  • Callback — optional routing targets for async notification callbacks: {service, controller, action, validation_id}.

Invoice lifecycle

Two parallel state machines: the invoice status and the emission status.

Invoice status

pending
requested
succeeded
pending
requested
failed

Cancellation branch

pending-cancelation
requested-cancelation
canceled
pending-cancelation
requested-cancelation
failed-cancelation

Emission status (sub-document)

not-requested
requested
authorized
requested
failed
requested
denied

An emission can only be canceled when its status is authorized.

Emission flow — POST /invoices end to end

client
Sends POST /fiscal-entities/:id/invoices with items, buyer (optional), fiscal_date, callback (optional).
system
Returns 400 invoices.emission_not_active if the seller's emission_status isn't active.
system
Resolves seller, buyer, tax items, partners. Builds the invoice with status: pending, computes amounts and taxes, performs final validation.
system
Persists the invoice, returns pending to the client immediately.
system (async)
Emission continues asynchronously. The pipeline copies the seller's tax data, populates buyer fiscal data and address, and looks up the provider (eNotas) configuration.
system
Selects the per-city template, builds description, notes, and processes the request for that city's requirements.
system / eNotas
Snapshots the request into emissions[] as not-requested, then calls the provider to emit. Status becomes requested.
eNotas (async)
Processes the NFS-e and sends a callback with the final status. A safety-net worker polls every 2 minutes as a fallback.

Per-city templates

NFS-e requirements vary by municipality. The emission pipeline handles city-specific formatting automatically for the supported cities:

Rio de Janeiro, Taboão da Serra, Vitória da Conquista, Andradas, Santana de Parnaíba, Caxias do Sul, Sobral, Cabo Frio, São Paulo, Osasco + a Default fallback.

Invoice cancellation

POST /fiscal-entities/:id/invoices/:invoice_id/cancel begins the cancellation flow.

Flow

  1. The system validates that the invoice is not already canceled.
  2. Sets the invoice status to pending-cancelation immediately.
  3. If the seller's emission_status is inactive, the cancellation completes without contacting the provider.
  4. Otherwise cancels with the provider and sets the emission to requested-cancelation.
  5. Sets the invoice status to requested-cancelation.
  6. The provider callback or the safety-net worker resolves the final state: canceled or failed-cancelation.

Safety-net worker

A safety-net worker polls every 2 minutes as a fallback to recover from missed callbacks. This means that even if a callback is lost, the invoice will still resolve to a final state within 2 minutes.

Callbacks & status events

When an invoice has a Callback set (the requester passed one in POST /invoices), every status change fires a notification to that callback target. The routing is stored on the invoice as {service, controller, action}.

Events fired

EventTrigger
successInvoice reached succeeded
denyInvoice reached denied
provider_errorProvider returned an error
request_errorRequest-side error (validation, build)
cancelInvoice reached canceled
provider_cancel_errorProvider rejected the cancelation
request_cancel_errorRequest-side error during cancelation

Each event also appends a Calls history entry on the invoice with the timestamp, status, and metadata.

JSON:API shapes

Create invoice request

POST /fiscal-entities/123/invoices
{
  "data": {
    "type": "invoices",
    "attributes": {
      "fiscal-date": "2024-10-15",
      "items-source": "manual",
      "items": [
        { "description": "Manicure", "quantity": 1, "unit-price": 5000 }
      ],
      "options": { "notes": ["Cliente VIP"] },
      "callback": { "service": "business", "controller": "invoices", "action": "process_callback" }
    }
  }
}

Invoice response

{
  "data": {
    "id": "123456",
    "type": "invoices",
    "attributes": {
      "reference-key": "1234-01-1734...",
      "type": "services",
      "status": "pending",
      "fiscal-date": "2024-10-15",
      "amount": 5000,
      "items-source": "manual",
      "items": [...],
      "taxes": [],
      "partners": [],
      "data": { "number": null, "code": null, "emission_date": null, "cancelation_date": null },
      "urls": { "pdf": null, "xml": null },
      "options": { "notes": [] },
      "extra": {}
    },
    "relationships": {
      "seller": { "data": { "type": "fiscal-entities", "id": "123" } },
      "buyer":  { "data": null }
    }
  }
}

Once the invoice reaches succeeded, data and urls are populated with the provider-issued data and download links.

Staging vs production

In staging, emission is simulated without contacting the provider. In production, invoices are emitted with the real provider.

Failure modes & gotchas

SymptomCauseWhat to do
400 invoices.emission_not_activeSeller's emission_status isn't activeRun the setup first
Invoice stuck in requestedProvider callback was lostWait up to 2 minutes — the safety-net worker will resolve it
Invoice reaches failedProvider returned an error (invalid CNPJ, service code, etc.)Inspect emissions[].providerErrors for the error code and message
Cancel reaches failed-cancelationProvider rejected the cancelation (e.g. past the deadline)Inspect emissions[].providerErrors; in Brazil some cities impose a 24h cancelation window
400 IdInvalidURL has :id placeholder or non-numeric idSubstitute the actual path parameter
Invoice amount is forced to 100items_source != "bill" and sellerId ≤ 1000Test sellers are clamped to R$1,00 for safety