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/paymentsThe configure command does five things:
- registers three providers in
adonisrc.ts—payments_provider,dashboard_providerandpayments_client_provider(the last two read their own config file and the client one is off unless you enable it), plus the Assemblerinithook that generates theapp/payment_handlers/barrel; - registers the ace commands (
make:billable,make:webhook-handler,payments:webhook,payments:sync,payments:health); - publishes
config/payments.ts,config/payments_dashboard.tsandconfig/payments_client.ts; - publishes one Lucid migration for the billing tables — which most apps can delete, see the next step;
- registers the payment env validations.
Credentials are wired through config/payments.ts — you 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.
PAYMENTS_ASAAS_KEY=your-asaas-api-key
PAYMENTS_ASAAS_WEBHOOK_TOKEN=your-asaas-webhook-tokenimport 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:
billing: { autoCreateSchema: false }node ace migration:runconfigure 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:webhookThe 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).
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 customerAdd 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:
- validates the signature (rejecting forged callbacks),
- records the event in the idempotency ledger (a redelivery is a no-op),
- syncs the local billing tables to the confirmed state,
- 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@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.
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.
Payments
Multi-gateway payments and Cashier-style billing for AdonisJS — 18 gateways behind one driver contract, across Brazil, Europe, North America, Latin America and India, with invoice emission, method routing and webhook-driven business logic.
Concepts
The four ideas the library is built on — money is an integer, the gateway is behind one contract, a charge is a promise until the webhook, and the ledger is what makes trusting the webhook safe.