Guía técnica completa para integrar Polar en una app Next.js: SDK, checkout, webhook, testing local y errores comunes.
Polar es una de las dos pasarelas de pago más usadas en SaaS indie en 2026 (la otra es Stripe). Su gran ventaja: como es Merchant of Record, gestiona el IVA por ti. Si vendes a Europa, te ahorra dolores de cabeza.
Esta guía explica cómo integrar Polar en una app Next.js paso a paso. Asume que ya tienes una cuenta de Polar y un producto creado.
bun add @polar-sh/sdkEn tu .env.local:
POLAR_ACCESS_TOKEN=polar_oat_...
POLAR_WEBHOOK_SECRET=whsec_...
POLAR_SERVER=productionPara desarrollo, usa POLAR_SERVER=sandbox. Las claves del sandbox son distintas, configúralas en
una .env.development.local.
src/lib/polar/client.ts:
import { Polar } from '@polar-sh/sdk';
export const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN!,
server: process.env.POLAR_SERVER as 'production' | 'sandbox',
});src/app/api/checkout/route.ts:
import { polar } from '@/lib/polar/client';
import { auth } from '@/lib/auth/server';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { productId } = await req.json();
const checkout = await polar.checkouts.create({
productId,
customerEmail: session.user.email,
successUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/checkout/success`,
});
return NextResponse.json({ url: checkout.url });
}Polar te avisa por webhook cuando alguien paga, cancela, o cuando una suscripción cambia. Sin webhook bien configurado, no te enteras y tu DB queda desincronizada.
src/app/api/webhooks/polar/route.ts:
import { polar } from '@/lib/polar/client';
import { db } from '@/lib/db/client';
import { headers } from 'next/headers';
export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers();
const signature = headersList.get('webhook-signature');
// Validar firma
const event = polar.webhooks.verify({
body,
signature: signature!,
secret: process.env.POLAR_WEBHOOK_SECRET!,
});
switch (event.type) {
case 'subscription.created':
await db.subscription.create({
data: {
userId: event.data.metadata.userId,
polarSubscriptionId: event.data.id,
status: 'active',
},
});
break;
case 'subscription.canceled':
await db.subscription.update({
where: { polarSubscriptionId: event.data.id },
data: { status: 'canceled' },
});
break;
case 'order.created':
// Compra única
await db.purchase.create({
data: {
userId: event.data.metadata.userId,
polarOrderId: event.data.id,
amount: event.data.amount,
},
});
break;
}
return new Response('OK', { status: 200 });
}Para que Polar pueda enviarte webhooks a localhost, usa un túnel como ngrok o Cloudflare Tunnel:
ngrok http 3000Y configura el webhook URL en Polar Dashboard → Webhooks → la URL pública de ngrok +
/api/webhooks/polar.
Cualquier compra de prueba en sandbox dispara el webhook y deberías ver el evento llegar.
'use client';
import { useRouter } from 'next/navigation';
export function CheckoutButton({ productId }: { productId: string }) {
const router = useRouter();
async function handleCheckout() {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId }),
});
const { url } = await res.json();
router.push(url);
}
return <button onClick={handleCheckout}>Comprar</button>;
}1. El webhook no llega: comprueba que la URL pública es accesible y que el signing secret coincide.
2. La firma es inválida: asegúrate de pasar el body como string crudo, no parseado, al verificar la firma.
3. El usuario aparece pero la suscripción no: verifica que pasas metadata: { userId } al crear
el checkout. Sin eso, no sabes a qué usuario pertenece la suscripción.
Si tu volumen sube mucho, Stripe vuelve a ser interesante por las comisiones más bajas. Pero Polar es la opción rápida para arrancar.
Integrar Polar en Next.js son ~150 líneas de código si ya tienes auth. El webhook es la pieza más delicada: dedícale tiempo a probarlo en sandbox antes de meter en producción.
Una vez funcionando, te olvidas del IVA. Y eso, si vendes a Europa, vale lo que pesa.
Suscríbete para más tutoriales y tips sobre crear productos con IA
La integración real de Tailwind v4 + shadcn 2.3.0 en Next.js 16 App Router. OKLCH, dark mode sin FOUC, Geist con next/font y el bug silencioso de hsl(var()) que rompe tus estilos sin avisar.