Guía técnica completa para integrar Polar en una app Next.js: SDK, checkout, webhook, testing local y errores comunes.
Israel Palma
3 min de lectura
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.
## Paso 1: instalar el SDK
```bash
bun add @polar-sh/sdk
```
## Paso 2: configurar variables de entorno
En tu `.env.local`:
```
POLAR_ACCESS_TOKEN=polar_oat_...
POLAR_WEBHOOK_SECRET=whsec_...
POLAR_SERVER=production
```
Para desarrollo, usa `POLAR_SERVER=sandbox`. Las claves del sandbox son distintas, configúralas en
una `.env.development.local`.
## Paso 3: crear el cliente
`src/lib/polar/client.ts`:
```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',
});
```
## Paso 4: crear el endpoint de checkout
`src/app/api/checkout/route.ts`:
```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 });
}
```
## Paso 5: el webhook
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`:
```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 });
}
```
## Paso 6: Probar localmente
Para que Polar pueda enviarte webhooks a localhost, usa un túnel como ngrok o Cloudflare Tunnel:
```bash
ngrok http 3000
```
Y 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.
## Paso 7: el botón de checkout en el frontend
```tsx
'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 ;
}
```
## Errores comunes
**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.
## Cuándo Polar es mejor que Stripe
- Vendes principalmente a Europa
- Volumen mensual < 5.000 €
- Quieres simplicidad por encima de features avanzadas
- No quieres lidiar con IVA en tu gestoría
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.
## Conclusión
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.
¿Te gustó este artículo?
Suscríbete para más tutoriales y tips sobre crear productos con IA