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
POST /invoices returns pending immediately; the final state comes via a callback or a safety-net worker.
Create
POST /fiscal-entities/{id}/invoices — the invoice is persisted with status: pending and emission happens asynchronously.
Emit (async)
The system builds the NFS-e request, calls the provider (eNotas), and sets the status to requested.
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
| Method | Path | Purpose |
|---|---|---|
| GET | /fiscal-entities/:id/invoices | List invoices for the fiscal entity. Filters: tenant_id (or seller_id from foreign_id), pagination. |
| GET | /fiscal-entities/:id/invoices/:invoice_id | Get a single invoice. 404 if not found. |
| POST | /fiscal-entities/:id/invoices | Create & emit a new invoice. Persists as pending, emission happens asynchronously. |
| POST | /fiscal-entities/:id/invoices/:invoice_id/cancel | Begin the cancellation flow. |
| POST | /fiscal-entities/:id/invoices/:invoice_id/emit | Re-try emission on a failed invoice. |
The Invoice model
Table: accounting_invoices. One row per NFS-e request.
Core fields
Type—services|products.Status— see the lifecycle below.SellerId— theSellerFiscalEntityid.BuyerId— optionalBuyerFiscalEntityid.FiscalDate—"YYYY-MM-DD".Amount— in cents (integer). 1000 = R$ 10,00.ItemsSource—checkout|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 hasstatus,provider,providerId,providerErrors,providerStatuses(history), andproviderRequest(the full payload sent).Data— provider-issued data: number, code, emission_date, cancelation_date.Urls—pdfandxmldownload 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
Cancellation branch
Emission status (sub-document)
An emission can only be canceled when its status is authorized.
Emission flow — POST /invoices end to end
POST /fiscal-entities/:id/invoices with items, buyer (optional), fiscal_date, callback (optional).400 invoices.emission_not_active if the seller's emission_status isn't active.status: pending, computes amounts and taxes, performs final validation.pending to the client immediately.emissions[] as not-requested, then calls the provider to emit. Status becomes requested.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
- The system validates that the invoice is not already
canceled. - Sets the invoice status to
pending-cancelationimmediately. - If the seller's
emission_statusisinactive, the cancellation completes without contacting the provider. - Otherwise cancels with the provider and sets the emission to
requested-cancelation. - Sets the invoice status to
requested-cancelation. - The provider callback or the safety-net worker resolves the final state:
canceledorfailed-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
| Event | Trigger |
|---|---|
success | Invoice reached succeeded |
deny | Invoice reached denied |
provider_error | Provider returned an error |
request_error | Request-side error (validation, build) |
cancel | Invoice reached canceled |
provider_cancel_error | Provider rejected the cancelation |
request_cancel_error | Request-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
| Symptom | Cause | What to do |
|---|---|---|
400 invoices.emission_not_active | Seller's emission_status isn't active | Run the setup first |
Invoice stuck in requested | Provider callback was lost | Wait up to 2 minutes — the safety-net worker will resolve it |
Invoice reaches failed | Provider returned an error (invalid CNPJ, service code, etc.) | Inspect emissions[].providerErrors for the error code and message |
Cancel reaches failed-cancelation | Provider rejected the cancelation (e.g. past the deadline) | Inspect emissions[].providerErrors; in Brazil some cities impose a 24h cancelation window |
400 IdInvalid | URL has :id placeholder or non-numeric id | Substitute the actual path parameter |
| Invoice amount is forced to 100 | items_source != "bill" and sellerId ≤ 1000 | Test sellers are clamped to R$1,00 for safety |