Payments · modules/billing
Production-ready adapterMercadoPago,
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.
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)?+
How do I test without touching production?+
Can I run MercadoPago and Stripe at the same time?+
Configuration reference
Environment variables
PAYMENT_PROVIDERrequiredSet to mercado-pago
MERCADO_PAGO_ACCESS_TOKENrequiredMERCADO_PAGO_WEBHOOK_SECRETrequiredHMAC secret for x-signature verification
Where it lives
apps/server/src/modules/billing/infrastructure/providers/mercado-pago-payment-provider.tsapps/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.