Payments · modules/billing

Production-ready adapter

MercadoPago,
wired the right way.

Most Next.js SaaS starters are Stripe-first. When you need to charge in pesos or reais, gluing MercadoPago into Next.js by hand is where the classic bugs live — unverified webhooks, non-idempotent handlers, and a sandbox that fights you. UseDeploy ships the adapter so you inherit the plumbing, not the pain.

The pain we already ate

MercadoPago integration has a reputation, and the developer forums earn it: "de longe essa é a mais complexa e difícil de testar." The async webhook is where it bites — the truth about a payment lands after the redirect, on a separate signed request you have to verify and reconcile against your own database. The UseDeploy adapter is a real, working implementation of that flow: Checkout Pro preferences for the redirect, a timing-safe HMAC check on every incoming notification, and a payment-status fetch that maps the result onto the same normalized BillingEvent every other provider emits. You wire your access token; the rail is already built.

Signed webhooks, verified before they are trusted

Every MercadoPago notification is verified against the x-signature / x-request-id manifest with HMAC-SHA256 and a constant-time comparison before the payment is fetched and mapped to a BillingEvent. An unsigned or tampered call is rejected with a 400 — the double-charge and spoofed-webhook classes are closed by default.

mercado-pago-payment-provider.ts
 1  const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`;
 2  const expected = crypto
 3    .createHmac('sha256', this.cfg.webhookSecret)
 4    .update(manifest)
 5    .digest('hex');
 6  
 7  // constant-time comparison — never a naive === on the digest
 8  return crypto.timingSafeEqual(hashBuf, expectedBuf);

What the adapter does today

Checkout Pro redirect

createCheckoutSession posts to the /checkout/preferences API and returns the init_point URL. One-time payment mode is live; currency_id covers ARS, BRL, MXN, USD and EUR.

Signed async webhooks

parseWebhook verifies the HMAC signature, fetches /v1/payments/:id, and emits invoice.paid or invoice.payment_failed onto the billing event bus.

One typed contract

MercadoPago implements the same IPaymentProvider port as Stripe and Polar — swapping providers is a one-file change in the composition root.

Durable event bus

Billing events flow through the durable outbox, so a webhook that arrives twice or out of order reconciles against your DB instead of double-applying.

Plan catalog from Prisma

listPlans reads the BillingPlan table, so your pricing lives in your database — not hardcoded per provider.

Trilingual product docs

The Fumadocs site ships real EN / ES / PT pages (dot-parser convention, EN canonical), not just locale routing on English copy.

Straight answers

Does it support recurring subscriptions (preapproval)?+
Not yet. As of July 2026 the MercadoPago adapter implements one-time Checkout Pro payments; subscription mode returns an explicit "not yet supported" error rather than silently mis-charging. The typed contract already models subscriptions, so preapproval is an adapter-level addition — Stripe and Polar cover recurring billing today.
How do I test without touching production?+
You point MERCADO_PAGO_ACCESS_TOKEN at your sandbox credentials. Because the provider selection is a single env-driven factory, switching between sandbox and live is a config change, not a code change.
Can I run MercadoPago and Stripe at the same time?+
The boilerplate selects one active PAYMENT_PROVIDER per deployment. All three adapters ship in the tree behind the same interface, so you choose per environment (for example MercadoPago for your LATAM tenant, Stripe or Polar for global).

Configuration reference

Environment variables

  • PAYMENT_PROVIDERrequired

    Set to mercado-pago

  • MERCADO_PAGO_ACCESS_TOKENrequired
  • MERCADO_PAGO_WEBHOOK_SECRETrequired

    HMAC secret for x-signature verification

Where it lives

  • apps/server/src/modules/billing/infrastructure/providers/mercado-pago-payment-provider.ts
  • apps/server/src/modules/billing/infrastructure/providers/index.ts

Stop gluing MercadoPago into Next.js by hand.

Start on a base where checkout, signed webhooks and the billing contract are already wired — and localized in EN, ES and PT.