Agora

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:

app/services/checkout_service.ts
// 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:

app/services/checkout_service.ts
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:

app/services/checkout_service.ts
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:

config/payments.ts
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 via ENOTAS_API_KEY.
  • PlugNotas (invoice.plugnotas({ apiKey })) — NFS-e via PLUGNOTAS_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 with invoice: true uses the same gateway that took the payment (also works as automatic emission per subscription configured in the Asaas dashboard).
config/payments.ts
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.

On this page