Agora

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.

@adonis-agora/payments brings multi-gateway payments and Cashier-style billing to AdonisJS. One contract — charge, refund, createSubscription, parseWebhook — normalized across every supported gateway, so your services talk to one interface instead of one SDK per provider.

Nothing in the contract is region-specific: money is an integer in the currency's smallest unit, a payment method is a name the driver either supports or refuses, and a webhook is a signature to verify and an event to normalize. What differs between a card charge in the euro area and a Pix charge in Brazil is which driver you route to — not the code that calls it. A charge can go out through one gateway and emit its invoice through a completely different provider, in a single call.

The one rule

A payment is confirmed only by the webhook. A charge() returns a PENDING record — the money hasn't moved until the gateway calls your endpoint. The processor's idempotency ledger is what makes "trust only the webhook" safe: forged callbacks are rejected and redeliveries are no-ops. Point billing.dispatcher at durable and a half-processed confirmation survives a restart as well. See Webhooks.

The problem it solves

Payment integrations are usually a mess of gateway SDKs, hand-rolled status mirrors, and webhook controllers that duplicate each other. @adonis-agora/payments collapses that into four guarantees:

  • One driver contract, every gateway. Every gateway normalizes onto the same PaymentsDriver. Adding a gateway is a config entry, not a code change — and a driver declares which methods it supports, so routing a card to a Pix-only gateway fails at the manager, not at the gateway.
  • Money moves only on confirmation. The idempotent webhook processor (a gateway-event ledger written before any work runs) makes redeliveries no-ops. Your business logic — grant credits, activate a subscription — runs at the right moment, exactly once. And the endpoint that receives it will not boot unverified: a driver that can authenticate a delivery and was given no credential refuses to start.
  • Billing mirrors the gateway. Subscriptions, payments, disputes and trials sync into Lucid tables automatically, Cashier-style, with billable mixins on your models — and the library creates those tables itself, so adopting it is a config file, not a migration.
  • Invoices are a separate concern. Pay through one provider, emit the invoice through another — the invoice option on the charge call joins them.

Quickstart

The minimal loop — install, configure a gateway, charge — with one provider and no invoice provider:

Install and configure:

node ace add @adonis-agora/payments

This registers the providers in adonisrc.ts, publishes config/payments.ts (plus the dashboard and client config files), and registers the ace commands. There is no migration to run: the billing layer creates its own tables on first use.

Set a gateway secret and make your first charge:

.env
ASAAS_API_KEY=your-asaas-api-key
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('ASAAS_API_KEY') }),
  },
  methods: {
    pix: 'asaas',
    credit_card: 'asaas',
  },
})
app/services/checkout_service.ts
import { getPayments } from '@adonis-agora/payments/services/main'

const customer = await getPayments().driver('pix').createCustomer({
  name: 'Jane Doe',
  email: 'jane@example.com',
  taxId: '123.456.789-00',
})

// charge via Pix — the customer scans the QR, then the webhook confirms
const payment = await getPayments().driver('pix').charge({
  customerId: customer.id,
  amount: 1990, // R$ 19,90 — integer cents, never floats
})
// `driver('pix')` binds the method: the charge goes out as Pix without repeating it.

Register the webhook and react to confirmations:

node ace payments:webhook   # prints the URL + event list per provider
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: WebhookEvent): Promise<void> {
    // dispatch a durable workflow — durable owns retry + exactly-once
  }
}

The happy path, end to end

1

you open a charge

your appcharge()gatewayStripe · Pix · Asaas

payment.status = 'pending' — the record exists, but nothing has been paid. Never grant anything here.

out-of-band — seconds, or days
2

the customer acts

The customer scans the Pix QR, or approves the card — outside your process, on their own clock. Your app is not in this step and cannot observe it.

the gateway calls you back
3

the webhook confirms it

your app/payments/webhookwebhookgatewaypaid

One request, five ordered steps — each one is a guarantee:

  1. 1validate the signatureforged callbacks rejected
  2. 2ledger the eventredeliveries stop here
  3. 3sync the billing tablesbilling_payments / billing_subscriptions
  4. 4run your handlergrant credits, activate the subscription
  5. 5publish diagnosticsagora:payments:payment.succeeded
Nothing has been paid until the webhook arrives — charge() only opens a pending record. The five steps are what make trusting that callback safe.

Getting Started

Install, configure a gateway, register webhooks and make your first charge — with no migration to run.

Concepts

Money as an integer, one contract over every gateway, the pending-until-webhook rule, and the idempotency ledger.

Configuration

Providers, method routing, the invoice section, and the billing layer — who owns the schema, which dispatcher runs a webhook, where handlers live.

Providers

18 gateways side by side — methods, subscriptions, disputes, externalReference.

Webhooks

Signature validation, a delivery that carries several events, the idempotency ledger, and the four homes for your business logic.

Billing

Billable mixins, the billing tables, webhook sync, and durable-backed dispatch.

Invoices

Emit an invoice attached to a charge, through a provider independent of the gateway.

Client

The browser status endpoint and the usePaymentStatus polling hook — ownership first, off until you enable it.

Dashboard

The console at /payments — health first, then revenue, payments, customers, subscriptions, disputes, the webhook ledger and the audit trail, with refund, retry and dispute resolution.

Diagnostics

Every milestone on the observability bus — gateway-action vs business events, Telescope.

Custom providers

Write a gateway or invoice provider as a config factory, or extend a bundled driver.

Testing

The fake driver, a real manager over it, the in-memory store, awaiting dispatched webhooks — exercise billing without a gateway or DB.

Patterns

A cookbook with the gateway differences side by side — Pix, subscriptions, where business logic lives, metered billing, splits, recovery.

Production

The operational checklist — webhook secrets, choosing a dispatcher, reconciliation, what to watch.

Troubleshooting

The symptoms that actually happen, and how to tell the causes apart.

CLI

payments:webhook, payments:sync, payments:health, make:billable and make:webhook-handler.

API reference

Every config option, driver method, exported helper, type, event and command — from source.

On this page