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') }),
},
}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)). 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.