Checkout Pro do Mercado Pago no Next.js (com Pix)
Checkout Pro do Mercado Pago no Next.js: preferência no servidor, preço do seu catálogo, back_urls, expiração, Pix e por que o webhook confirma.
O Checkout Pro é a integração do Mercado Pago em que o pagamento acontece numa
página do Mercado Pago: o seu servidor cria uma preferência, você redireciona
o usuário para a URL que ela devolve (init_point), ele paga ali com o que tiver
e volta para o seu site. É a forma mais rápida de cobrar sem lidar com dados de
cartão.
No Brasil, a documentação oficial do Checkout Pro (https://www.mercadopago.com.br/developers/pt/docs/checkout-pro/overview) lista como meios de pagamento cartão de crédito ou débito, Pix, boleto, Conta Mercado Pago e Linha de Crédito. Ou seja, o seu cliente pode pagar com Pix sem você integrar nada à parte: é a mesma preferência e o mesmo webhook.
Este guia mostra como fazer isso no Next.js com o App Router, com as decisões que tomamos no adapter do useDeploy depois de cometer os erros típicos.
O fluxo em quatro passos
- O usuário escolhe o que comprar. O seu servidor cria a preferência com
POST https://api.mercadopago.com/checkout/preferences. - Você redireciona o usuário para o
init_pointda resposta. - Ele paga no Mercado Pago e volta para uma das suas
back_urls. - O Mercado Pago manda um webhook. Esse webhook, e não a volta do usuário, é o que confirma o pagamento.
O passo 4 é o que mais se pula, e é o que importa. Com Pix isso pesa ainda mais: o usuário pode pagar pelo app do banco e nem voltar ao seu site, e só o webhook avisa.
Criar a preferência no servidor
Uma Server Action (ou um Route Handler) que recebe só o id do plano:
// app/checkout/actions.ts
'use server';
import 'server-only';
import { redirect } from 'next/navigation';
import { getPlanById } from '@/server/billing/plans'; // seu catálogo
import { getSession } from '@/server/auth';
const MP_API_BASE = 'https://api.mercadopago.com';
export async function startCheckout(planId: string) {
const session = await getSession();
if (!session) redirect('/login');
// O preço sai do SEU catálogo, nunca do cliente.
const plan = await getPlanById(planId);
if (!plan?.active) throw new Error('Plano inexistente');
const appUrl = process.env.APP_URL!;
const res = await fetch(`${MP_API_BASE}/checkout/preferences`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MERCADO_PAGO_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{
id: plan.id,
title: plan.name,
quantity: 1,
unit_price: plan.amountCents / 100, // o Mercado Pago usa unidades maiores
currency_id: plan.currency.toUpperCase(), // 'BRL' para cobrar em reais
},
],
payer: { email: session.user.email },
external_reference: session.user.id, // a quem creditar o pagamento
back_urls: {
success: `${appUrl}/checkout/resultado`,
failure: `${appUrl}/checkout/resultado`,
pending: `${appUrl}/checkout/resultado`,
},
auto_return: 'approved',
notification_url: `${process.env.API_URL}/billing/webhook`,
metadata: { plan_id: plan.id },
expires: true,
expiration_date_to: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
}),
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) throw new Error(`Mercado Pago ${res.status}: ${await res.text()}`);
const { init_point } = (await res.json()) as { init_point: string };
redirect(init_point);
}O que cada decisão faz:
import 'server-only': se alguém importar este módulo de um componente de cliente, o build falha. O access token nunca chega ao navegador.- O preço sai do catálogo do servidor. Se o valor viesse do cliente, qualquer
pessoa poderia pagar $1 por um plano de $199 pelo DevTools. O cliente manda um
planId; o servidor decide quanto vale. currency_id: a moeda em que a preferência cobra. Para cobrar em reais,BRL; a conta do Mercado Pago precisa ser do país correspondente.external_reference: o id do seu usuário ou pedido. Volta no pagamento, e com isso você sabe a quem creditar quando o webhook chega.auto_return: 'approved': se o pagamento é aprovado, o Mercado Pago devolve o usuário paraback_urls.successsem que ele precise tocar em "voltar ao site".notification_url: onde o Mercado Pago avisa das mudanças no pagamento. Precisa ser uma URL pública, então em desenvolvimento local você precisa de um túnel.expires+expiration_date_to: a preferência deixa de servir após 30 minutos. Evita que alguém pague amanhã um link com um preço antigo.metadata: dados seus que voltam no pagamento. Útil para saber qual plano foi comprado sem outra consulta.
A página de volta não confirma nada
Quando o usuário volta para /checkout/resultado, a URL traz parâmetros como
status e payment_id. Não os use para liberar acesso. Fazem parte da URL:
qualquer pessoa pode digitar ?status=approved na mão.
O correto é mostrar o estado do seu banco, que só é atualizado quando o webhook chega e é validado:
// app/checkout/resultado/page.tsx
import { getSession } from '@/server/auth';
import { getLatestPurchase } from '@/server/billing/purchases';
export default async function Resultado() {
const session = await getSession();
const purchase = session ? await getLatestPurchase(session.user.id) : null;
if (purchase?.paidAt) return <p>Pronto! Sua compra foi confirmada.</p>;
return <p>Estamos confirmando seu pagamento com o Mercado Pago. Pode levar alguns segundos.</p>;
}Se quiser que a página se atualize sozinha, adicione um refresh a cada poucos
segundos (router.refresh() num componente de cliente pequeno) até o paidAt
aparecer.
O webhook: onde o pagamento se confirma
O webhook do Mercado Pago não traz o pagamento, traz um aviso com o id. É preciso verificar a assinatura, ler o pagamento pela API e só então creditar, e fazer isso de forma idempotente porque ele pode chegar várias vezes. Está tudo detalhado em webhooks do Mercado Pago: como validar a assinatura.
Testar em desenvolvimento
- Use as credenciais de teste da sua aplicação e usuários de teste.
- Para o webhook chegar na sua máquina, exponha o endpoint com um túnel e coloque
essa URL em
notification_url. - Guarde um webhook real capturado e reenvie-o várias vezes: se o seu handler é idempotente, o resultado tem que ser uma única compra.
Perguntas frequentes
Checkout Pro ou Checkout API (Bricks)? O Checkout Pro é mais rápido de integrar e o Mercado Pago cuida da página de pagamento. Com Bricks ou a Checkout API o pagamento acontece dentro do seu site, mas você integra mais peças.
O Checkout Pro aceita Pix? No Brasil, sim, segundo a documentação oficial do Checkout Pro citada acima. É um dos meios de pagamento da própria página do Mercado Pago, junto com cartão e boleto.
Posso cobrar assinaturas com o Checkout Pro?
O Checkout Pro é para pagamentos únicos. Para cobranças recorrentes o Mercado
Pago tem uma API própria (preapproval), que é outra integração. O adapter do
useDeploy hoje não a implementa.
Por que o usuário volta com status=approved mas o meu banco não tem o
pagamento?
Porque a volta e o webhook são independentes, e o webhook pode levar alguns
segundos. Espere o webhook; não confirme com os parâmetros da URL.
No useDeploy esse fluxo já vem montado: a preferência com o preço do catálogo, a volta que espera o webhook e o webhook com assinatura verificada e idempotência, junto com auth, organizações e o resto de um SaaS. Veja a integração com o Mercado Pago. Se você está começando com a API, comece por API do Mercado Pago com Node.js e TypeScript (em espanhol).