Agora

Getting started

Install @adonis-agora/payments, configure a gateway, register webhooks and make your first charge in about five minutes — with no migration to run.

This guide takes you from zero to a charge that accepts Pix and card, with webhooks wired and your business logic reacting to a confirmed payment.

node ace add @adonis-agora/payments

The configure command does five things:

  1. registers three providers in adonisrc.tspayments_provider, dashboard_provider and payments_client_provider (the last two read their own config file and the client one is off unless you enable it), plus the Assembler init hook that generates the app/payment_handlers/ barrel;
  2. registers the ace commands (make:billable, make:webhook-handler, payments:webhook, payments:sync, payments:health);
  3. publishes config/payments.ts, config/payments_dashboard.ts and config/payments_client.ts;
  4. publishes one Lucid migration for the billing tables — which most apps can delete, see the next step;
  5. registers the payment env validations.

Credentials are wired through config/payments.tsyou name your .env variables however you like and reference them with env.get(...). The library's built-in fallbacks (ASAAS_API_KEY, STRIPE_KEY, …) are a convenience when you don't pass a value, not a contract.

The configuration below is for the gateway picked in the sidebar — Asaas until you pick one. Every gateway's own options are on its provider page.

Showing Asaas — pick your gateway in the sidebar to see yours.

.env
PAYMENTS_ASAAS_KEY=your-asaas-api-key
PAYMENTS_ASAAS_WEBHOOK_TOKEN=your-asaas-webhook-token
config/payments.ts
import env from '#start/env'
import { defineConfig, payments } from '@adonis-agora/payments'

export default defineConfig({
  default: 'asaas',
  providers: {
    asaas: payments.asaas({
      apiKey: env.get('PAYMENTS_ASAAS_KEY'),
      webhookToken: env.get('PAYMENTS_ASAAS_WEBHOOK_TOKEN'),
    }),
  },
  methods: { pix: 'asaas', credit_card: 'asaas' },
})

Asaas takes Pix, boleto and cards from one account, so routing both methods to it is the whole map. Outside production the driver talks to the sandbox host on its own (sandbox defaults to NODE_ENV !== 'production').

Or pass values from anywhere — the driver configs are plain objects, so an API key can come from a vault, another config file, or a literal (not recommended for secrets).

The webhook credential is not optional

The webhook credential above — webhookToken, webhookSecret, webhookPublicKey, whatever your gateway calls it — is what lets the driver authenticate a delivery, and a driver that can verify with nothing configured refuses to boot — the alternative is accepting anything anyone posts to /payments/webhook/<provider>, including a body that marks a payment paid.

Set the credential, or, only if verification really happens upstream, say so: allowUnverifiedWebhooks: ['asaas']. Efí and InfinitePay are exempt: their gateways sign nothing, so there is no credential to forget. See Configuration → allowUnverifiedWebhooks.

There isn't one to run. The billing layer owns seven tables — customers, subscriptions, payments, disputes, usage events, the audit trail and the webhook idempotency ledger — and it creates them itself on first use, the same convention @adonis-agora/durable and @adonis-agora/authz follow. Nothing to publish, nothing to run, nothing to remember when you upgrade.

The DDL is idempotent, so it costs one round trip on the first query of a process and nothing afterwards. Columns added to the library in later versions are carried to a database that already has the tables, which is what makes this an upgrade path rather than a first-install convenience.

If you would rather own the DDL — a shared database, a team that reviews every schema change, a deploy that runs migrations as their own step — turn it off and run the published migration instead:

config/payments.ts
billing: { autoCreateSchema: false }
node ace migration:run

configure publishes that one file for exactly this case. It calls the same createBillingTables the library does, so the two cannot drift. Leaving both on is harmless — whichever runs second finds the tables already there — but "who creates this table" is worth having one answer to.

If you want the gateways and none of the persistence, set billing: { enabled: false } and delete the published migration. The drivers work standalone; what you give up is everything the tables carry — the idempotency ledger included, so the mounted webhook route will verify a delivery and then do nothing with it.

Each gateway dashboard needs the endpoint URL and event list. Print them for every configured provider:

node ace payments:webhook

The provider mounts POST /payments/webhook/:provider automatically — register it in each gateway's dashboard and point it at /payments/webhook/<provider> (e.g. https://you.app/payments/webhook/asaas).

app/services/checkout_service.ts
import { getPayments } from '@adonis-agora/payments/services/main'

const payments = getPayments()

// Your own row, created before the money is asked for.
const order = await Order.create({ total: 1990, status: 'awaiting_payment' })

// A customer at the routed provider for `pix`
const customer = await payments.driver('pix').createCustomer({
  name: 'Jane Doe',
  email: 'jane@example.com',
  taxId: '123.456.789-00',
})

// Charge via Pix. `driver('pix')` threads the method into the charge, so you do not
// repeat `method: 'pix'` here.
const payment = await payments.driver('pix').charge({
  customerId: customer.id,
  amount: order.total, // 1990 is R$ 19,90 — integer cents, never floats
  externalReference: order.id, // comes back on every webhook for this charge
})

// payment.pixCode → the copy-paste BR code to show the customer

Add invoice: true to that call to emit a fiscal note with it — it needs an invoice section in the config first, or the charge throws. See Invoices.

Where does a payment become confirmed?

The charge above creates a PENDING payment at the gateway. Nothing has been paid yet — the customer still has to scan the QR code, or enter their card. The moment the money actually moves, the gateway calls your webhook endpoint, and the library:

  1. validates the signature (rejecting forged callbacks),
  2. records the event in the idempotency ledger (a redelivery is a no-op),
  3. syncs the local billing tables to the confirmed state,
  4. runs your business logic (grant the credits, activate the subscription).

That last step is where the app's value lives, and it has four homes with different failure behaviour — documented in depth in Webhooks. The fastest path to something real is the convention folder:

node ace make:webhook-handler payment.succeeded
app/payment_handlers/payment_succeeded.ts
@inject()
export default class PaymentSucceededHandler {
  static readonly eventType = 'payment.succeeded'
  async handle(event: WebhookEventFor<'payment.succeeded'>): Promise<void> {
    // dispatch a durable workflow — durable owns retry + exactly-once
  }
}

The provider picks the file up at boot — no registration step. WebhookEventFor<T> types event.data from the event type, so there is no cast to write; and eventType is checked at boot, so a misspelling throws instead of registering a handler nothing ever calls.

Never trust a payment until the webhook

A PENDING charge is a promise, not revenue. Grant access only after the gateway confirms via webhook — that's the guarantee the processor's idempotency ledger exists to make safe. See Webhooks and Billing for the details.

Next steps

  • Configuration — providers, method routing, invoices, the billing layer.
  • Providers — the eighteen gateways side by side, and what each one cannot do.
  • Webhooks — signature validation, idempotency, and the four places your business logic can live.
  • Billing — subscriptions, trials, the billable mixins.
  • Invoices — emitting invoices with the charge.
  • Diagnostics — every milestone on the observability bus.

On this page