| name | mercadopago-integration |
| description | Integrate MercadoPago Checkout Pro (redirect-based) into Next.js applications with any PostgreSQL database (Supabase, AWS RDS, Neon, PlanetScale, self-hosted, Prisma, Drizzle, or raw pg). Use when the user needs to: (1) Add MercadoPago payment processing to a Next.js app, (2) Create a checkout flow with MercadoPago, (3) Set up payment webhooks for MercadoPago, (4) Build payment success/failure pages, (5) Create a shopping cart with payment integration, (6) Troubleshoot MercadoPago integration issues (auto_return errors, webhook failures, hydration mismatches, double submissions). Triggers on requests mentioning MercadoPago, Mercado Pago, payment integration with MP, Argentine/Latin American payment processing, or checkout with MercadoPago. Supports all MercadoPago countries: Argentina (ARS), Brazil (BRL), Mexico (MXN), Colombia (COP), Chile (CLP), Peru (PEN), Uruguay (UYU).
|
MercadoPago Checkout Pro - Next.js Integration
Redirect-based payment flow: buyer clicks "Pay", is redirected to MercadoPago, completes payment, returns to the app. A webhook confirms the payment status in the background.
Quick Start
For a minimal integration, just tell Claude:
Integrar MercadoPago en mi app
Claude will automatically explore your codebase to detect:
- Database adapter (Supabase, Prisma, or raw pg)
- Cart store location
- Existing routes and patterns
- Currency based on context
For more control, provide details:
Integrate MercadoPago Checkout Pro.
Database: Prisma. Currency: ARS. Success route: /pago-exitoso.
See references/usage-examples.md for more prompt templates.
Payment Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ PAYMENT FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
User clicks "Pay"
│
▼
┌──────────────────┐
│ POST /api/checkout│
└────────┬─────────┘
│
▼
┌──────────────────────────────────┐
│ 1. Create purchase in DB │
│ (status: pending) │
│ 2. Create preference in MP API │
│ 3. Save preference_id in DB │
│ 4. Return init_point URL │
└────────┬─────────────────────────┘
│
▼
┌──────────────────┐ ┌─────────────────────┐
│ Redirect to MP │─────▶│ User pays on MP │
└──────────────────┘ └──────────┬──────────┘
│
┌───────────────────────────┴───────────────────────────┐
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Redirect back to app │ │ MP sends webhook │
│ /payment-success?id=... │ │ POST /api/webhooks/mp │
└──────────┬──────────────┘ └──────────┬──────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Verify status via API │ │ Update purchase status │
│ GET /api/purchases/[id] │ │ in database │
└──────────┬──────────────┘ └─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Show UI based on status │
│ approved/pending/rejected│
└─────────────────────────┘
Before Starting
-
Determine the database adapter. Explore the codebase or ask the user:
- Supabase? See
references/database-supabase.md
- Prisma? See
references/database-prisma.md
- Raw PostgreSQL (pg, Drizzle, etc.)? See
references/database-postgresql.md
-
Gather or infer from the codebase:
| Detail | Why | Example |
|---|
| Currency | Preference creation | ARS, BRL, MXN (see references/countries.md) |
| Success/failure routes | back_urls in preference | /payment-success, /pago-exitoso |
| Brand name | Card statement descriptor | MY_STORE (max 22 chars) |
| Product/item table | FK in purchase_items | products, photos, courses |
| Cart store location | Hook reads items from it | src/store/cart.ts |
| DB client path | API routes import it | src/lib/supabase/server.ts, src/lib/prisma.ts |
Prerequisites
- Install dependencies:
npm install mercadopago zod
- Set environment variables (never prefix access token with
NEXT_PUBLIC_):
MERCADOPAGO_ACCESS_TOKEN=TEST-xxxx # from https://www.mercadopago.com/developers/panel/app
NEXT_PUBLIC_APP_URL=http://localhost:3000 # HTTPS in production
- Run database migration from
assets/migration.sql (works on any PostgreSQL database).
Production Requirements
- SSL Certificate: Required for
auto_return and secure webhooks
- Active MercadoPago seller account: Create here
- Publicly accessible webhook URL: MercadoPago must reach your
/api/webhooks/mercadopago
Implementation Steps
Step 1: Database Helper
Create: src/lib/db/purchases.ts
This abstracts all purchase DB operations. Implement using your DB adapter.
See the reference file for your adapter:
- Supabase:
references/database-supabase.md
- Prisma:
references/database-prisma.md
- Raw pg / other:
references/database-postgresql.md
The helper must export these functions:
interface PurchaseInsert {
user_email: string;
status: 'pending';
total_amount: number;
}
interface PurchaseUpdate {
status?: 'pending' | 'approved' | 'rejected';
mercadopago_payment_id?: string;
mercadopago_preference_id?: string;
user_email?: string;
updated_at?: string;
}
export async function createPurchase(data: PurchaseInsert): Promise<{ id: string }>;
export async function updatePurchase(id: string, data: PurchaseUpdate): Promise<void>;
export async function getPurchaseStatus(id: string): Promise<{ id: string; : } | >;
(): <>;
Step 2: MercadoPago Client
Create: src/lib/mercadopago/client.ts
import { MercadoPagoConfig, Preference, Payment } from 'mercadopago';
const client = new MercadoPagoConfig({
accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN!,
});
const preference = new Preference(client);
const payment = new Payment(client);
interface CreatePreferenceParams {
items: { id: string; title: string; quantity: number; unit_price: number }[];
purchaseId: string;
buyerEmail?: string;
}
export async function createPreference({
items, purchaseId, buyerEmail,
}: CreatePreferenceParams) {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
return preference.create({
body: {
items: items.map((item) => ({
id: item.,
: item.,
: item.,
: item.,
: ,
})),
...(buyerEmail ? { : { : buyerEmail } } : {}),
: {
: ,
: ,
: ,
},
...(baseUrl.() ? { : } : {}),
: purchaseId,
: ,
: ,
: ,
: ().(),
: (.() + * * * ).(),
},
: {
: purchaseId,
},
});
}
() {
payment.({ : paymentId });
}
Step 3: Checkout API Route
Create: src/app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { createPurchase, updatePurchase } from '@/lib/db/purchases';
import { createPreference } from '@/lib/mercadopago/client';
import { z } from 'zod';
const checkoutSchema = z.object({
items: z.array(z.object({
id: z.string(),
title: z.string().min(1),
quantity: z.number().positive(),
unit_price: z.number().positive(),
})).min(1),
email: z.string().email().optional(),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const validation = checkoutSchema.safeParse(body);
if (!validation.success) {
.({ : }, { : });
}
{ items, email } = validation.;
totalAmount = items.( sum + i. * i., );
purchase = ({
: email || ,
: ,
: totalAmount,
});
mpPreference = ({
items, : purchase., : email,
});
(purchase., {
: mpPreference.,
});
.({
: mpPreference.,
: mpPreference.,
: purchase.,
});
} (error) {
.(, error);
.({ : }, { : });
}
}
Step 4: Webhook Handler
Create: src/app/api/webhooks/mercadopago/route.ts
import { NextResponse } from 'next/server';
import { getPurchaseStatus, updatePurchase } from '@/lib/db/purchases';
import { getPayment } from '@/lib/mercadopago/client';
export async function POST(request: Request) {
try {
const body = await request.json();
if (body.type !== 'payment' && body.action !== 'payment.created' && body.action !== 'payment.updated') {
return NextResponse.json({ received: true });
}
const paymentId = body.data?.id;
if (!paymentId) return NextResponse.json({ received: true });
const payment = await getPayment(paymentId.toString());
if (!payment?.external_reference) return NextResponse.({ : });
: | | = ;
(payment. === ) status = ;
([, , ].(payment. || )) status = ;
existing = (payment.);
(existing?. === || existing?. === ) {
.({ : });
}
payerEmail = payment.?.;
(payment., {
status,
: paymentId.(),
...(payerEmail ? { : payerEmail } : {}),
: ().(),
});
.({ : });
} (error) {
.(, error);
.({ : });
}
}
() {
.({ : });
}
Step 5: Purchase Status API
Create: src/app/api/purchases/[id]/route.ts
import { NextResponse } from 'next/server';
import { getPurchaseStatus } from '@/lib/db/purchases';
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const data = await getPurchaseStatus(id);
if (!data) return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json({ id: data.id, status: data.status });
}
Step 6: Checkout Hook (Frontend)
Create: src/hooks/useCheckout.ts
Double-click prevention uses useRef (survives re-renders, unlike useState).
'use client';
import { useCallback, useRef, useState } from 'react';
export function useCheckout() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const guard = useRef(false);
const submitCheckout = useCallback(async (items: unknown[]) => {
if (guard.current) return;
setError(null);
guard.current = true;
setIsSubmitting(true);
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
});
const data = await res.json();
if (!res.ok) throw (data. || );
(data.) .. = data.;
();
} (err) {
(err ? err. : );
();
guard. = ;
}
}, []);
{ submitCheckout, isSubmitting, error };
}
Step 7: Success Page with Verification
Create: src/app/payment-success/page.tsx (adjust route name)
Always verify purchase status server-side. Never trust the redirect URL alone.
Wrap useSearchParams in <Suspense> (Next.js App Router requirement).
'use client';
import { useSearchParams } from 'next/navigation';
import { Suspense, useCallback, useEffect, useState } from 'react';
type Status = 'loading' | 'approved' | 'pending' | 'rejected' | 'error';
function PaymentResult() {
const purchaseId = useSearchParams().get('purchase');
const [status, setStatus] = useState<Status>(purchaseId ? 'loading' : 'approved');
const verify = useCallback(async (id: string) => {
try {
const res = await fetch(`/api/purchases/${id}`);
if (!res.ok) { setStatus('error'); return; }
const { status } = await res.json();
setStatus(status === 'approved' ? 'approved'
: status === 'pending' ? 'pending' : 'rejected');
} { (); }
}, []);
( { (purchaseId) (purchaseId); }, [purchaseId, verify]);
(status === ) {
;
}
(status === ) {
(
);
}
(status === ) {
(
);
}
(status === ) {
(
);
}
(
);
}
() {
;
}
Checklist
Configuration
Backend Implementation
Frontend Implementation
Production Readiness
Critical Gotchas
For detailed solutions, see references/troubleshooting.md.
| Gotcha | Fix |
|---|
auto_return + localhost = 400 error | Only set when URL starts with https |
user_email NOT NULL + no email = 500 | Use 'pending@checkout' placeholder; webhook updates it |
currency_id doesn't match account country | Use correct currency (ARS for Argentina, BRL for Brazil, etc.) |
| Hydration mismatch (localStorage cart) | Add mounted state guard before rendering cart content |
| Double purchase on double-click | Use useRef guard, not just useState |
| Success page trusts redirect URL | Always verify via /api/purchases/[id] |
| Webhook duplicate updates | Check if purchase is already terminal before updating |
| Webhooks can't reach localhost | Use ngrok: ngrok http 3000 |
useSearchParams error | Wrap component in <Suspense> |
| Payment stuck in pending | Normal for offline methods (OXXO, Rapipago, Boleto) |
| Mixed test/production credentials | Never mix - use all TEST or all PROD |
References
Database Adapters
references/database-supabase.md - Supabase DB helper implementation
references/database-prisma.md - Prisma DB helper implementation
references/database-postgresql.md - Raw PostgreSQL (pg, Drizzle, etc.) DB helper implementation
Configuration
references/countries.md - Currencies, test cards, payment methods by country
references/testing.md - Complete testing guide with test cards and simulated results
references/mcp-server.md - MercadoPago MCP Server for AI integration
Help
references/troubleshooting.md - 20+ common errors and solutions
references/usage-examples.md - Ready-to-use prompt templates
Assets
assets/migration.sql - Database schema template (standard PostgreSQL)
External Links