Invoices
Emit invoices attached to a charge or subscription, through an invoice provider that is fully independent of the payment gateway.
For Brazilian payment flows, a charge and its invoice are two different
problems — and this library keeps them two different providers. The payment goes through
the gateway (Asaas, Woovi, …); the invoice goes through an invoice provider (Focus,
Tecnospeed, …). The invoice option on a charge call attaches them: pay via Pix, emit
the note via Focus, in one call.
Requesting an invoice
The invoice option on charge, createSubscription and createCheckout accepts three
shapes:
// 1. Default invoice provider (config.invoice.default)
await payments.driver('pix').charge({
customerId: 'cus_x',
amount: 1990,
invoice: true,
})
// 2. A named invoice provider — payment via woovi, note via tecnospeed
await payments.driver('pix').charge({
customerId: 'cus_x',
amount: 1990,
invoice: 'tecnospeed',
})
// 3. Per-charge overrides for the named/default provider
await payments.driver('credit_card').charge({
customerId: 'cus_x',
amount: 1990,
invoice: {
provider: 'focus',
service: { description: 'Software license', code: '1.01' },
tax: { iss: 5 },
},
})Emission runs after the gateway created the charge, inside the same charge() call —
and it is not best-effort. If the invoice provider throws (a rejected CPF, a municipality
timing out, a missing token), the exception propagates out of charge() after the
payment already exists at the gateway. The caller sees a failed call over money that was
in fact taken.
So treat charge({ invoice: true }) as two operations that share a return value. If your
flow cannot tolerate that asymmetry, omit invoice and emit separately — the payment's
gatewayId is all InvoiceEmitInput.payment needs, and a retry then costs a note rather
than a second charge.
Two defaults worth knowing: with no service.description anywhere the emitted note is
described as "Payment", and requesting an invoice with no invoice.providers
configured throws with a message naming the config key rather than silently skipping.
The payer's data
The invoice recipient defaults to the customer's data. For a card checkout, card.holder
(cpfCnpj, name, email) is the fallback, so a transparent checkout that also wants a
invoice doesn't restate the holder:
await payments.driver('credit_card').charge({
customerId: 'cus_x',
amount: 1990,
card: { token, holder: { name, email, cpfCnpj, ... } },
invoice: true, // recipient from card.holder
})What comes back
The emitted invoice is attached to the returned payment as payment.invoice:
const payment = await payments.driver('pix').charge({
customerId: 'cus_x',
amount: 1990,
invoice: true,
})
payment.invoice?.number // '12345'
payment.invoice?.key // access key (chave de acesso)
payment.invoice?.hostedPdfUrl // hosted PDF, when the provider gives one
payment.invoice?.status // 'issued' | 'pending' | 'failed' | ...The field is hostedPdfUrl, not url. Only the diagnostics payload calls it url — see
below.
The same event is also published on the diagnostics bus (agora:payments:invoice.emitted)
with number/url, so receipts and notification flows can react to it without coupling to
the charge call site.
Invoice providers
config.invoice.providers is a named map of provider factories — the same lazy-factory
pattern as payment drivers:
invoice: {
default: 'focus',
providers: {
focus: invoice.focus({ token: env.get('FOCUS_NFE_TOKEN') }),
tecnospeed: invoice.tecnospeed({ token: env.get('TECNOSPEED_TOKEN') }),
},
defaults: {
service: { description: 'Software license' },
},
}Built-in providers:
- Focus (
invoice.focus({ token })) — NF-e / NFS-e / NFC-e across hundreds of municipalities via REST; no SDK dependency. - eNotas (
invoice.enotas({ apiKey })) — NFS-e/NF-e viaENOTAS_API_KEY. - PlugNotas (
invoice.plugnotas({ apiKey })) — NFS-e viaPLUGNOTAS_API_KEY. - Tecnospeed (
invoice.tecnospeed({ token })) — NFS-e via the Tecnospeed API. - Asaas (
invoice.asaas({ apiKey })) — native NFS-e through Asaas's own invoice API, so a charge through Asaas withinvoice: trueuses the same gateway that took the payment (also works as automatic emission per subscription configured in the Asaas dashboard).
invoice: {
default: 'asaas',
providers: {
focus: invoice.focus({ token: env.get('FOCUS_NFE_TOKEN') }),
enotas: invoice.enotas({ apiKey: env.get('ENOTAS_API_KEY') }),
plugnotas: invoice.plugnotas({ apiKey: env.get('PLUGNOTAS_API_KEY') }),
asaas: invoice.asaas({ apiKey: env.get('ASAAS_API_KEY') }),
},
}Asaas NFS-e
Asaas identifies the recipient by its own ids, never by the CPF/CNPJ: the invoice is
attached to the Asaas charge (payment, filled in automatically when the charge itself went
through Asaas) or to an Asaas customer (customer: { gatewayId: 'cus_…' }). With neither,
emit throws.
await payments.invoice('asaas').emit({
customer: { taxId: '123.456.789-09', gatewayId: 'cus_000000002750' },
amount: 150050, // cents
currency: 'brl',
service: {
description: 'Consultoria patrimonial — setembro/2026',
cityServiceCode: '17.01', // → municipalServiceCode
municipalServiceId: '1234', // optional, from "Listar serviços municipais"
municipalServiceName: 'Assessoria ou consultoria',
},
tax: { iss: 2, retainIss: false, pis: 0, cofins: 0, csll: 0, inss: 0, ir: 0 },
effectiveDate: '2026-10-10', // default: today in Brasília
deductions: 0, // cents; reduces the ISS base, not the total
observations: 'Competência 09/2026',
externalReference: 'fee-invoice-42', // idempotency
})- Taxes: Asaas requires
retainIss,iss,pis,cofins,csll,inssandir. The retentions default to none (false/0); the ISS rate never defaults — a missingtax.issthrows instead of printing a 0% on a legal document. Other keys (nbsCode, the tax-reform fields) are sent as given. - Idempotency: with
externalReference,emitfirst lists/invoices?externalReference=and returns the live note it finds (not cancelled, notERROR) instead of scheduling a second one. - Scheduled is not issued:
POST /invoicesschedules the note; it comes backpending. The city hall's answer arrives by webhook on the Asaas payments endpoint asnfse.*events:nfse.authorized(issued),nfse.failed(INVOICE_ERROR, withstatusDescription),nfse.canceled,nfse.cancellation_denied,nfse.cancellation_processing,nfse.created,nfse.updated,nfse.synchronized. The webhook processor runs no built-in sync for them — register a handler. - Cancel:
provider.cancel(id, { providerOnly? })→POST /invoices/{id}/cancel(providerOnly= Asaas'cancelOnlyOnAsaas). The other built-in providers throw "not supported".
Or your own
Five providers ship: Focus, eNotas, PlugNotas, Tecnospeed and Asaas. For anything else —
a municipal API, a provider your accountant already uses — a custom provider is a plain
factory implementing the InvoiceProvider contract (emit(input), find(id),
cancel(id, options?)). See
Custom providers.
Billing
The Cashier-style subscription layer — billable Lucid mixins, the billing tables, an idempotent webhook processor that keeps local rows in sync with the gateway, and durable-backed dispatch.
Custom providers
Write a custom payment or invoice provider as a plain config factory, using the exported building blocks — httpRequest, toDecimal, emitInvoiceIfRequested, ensureCustomer, webhook security helpers.