Pagamentos com Polar.sh em Node.js: Guia Completo de Backend
Atualizado em 3 de janeiro de 2026
Integração de pagamentos Polar.sh com backend Node.js
Este post expande meu fio no X sobre pagamentos com Polar.sh com exemplos de código completos, tratamento de erros e lições aprendidas do uso em produção.
Se você está construindo um SaaS ou vendendo produtos digitais, precisa de um provedor de pagamentos. Polar.sh se tornou minha escolha principal para projetos indie—veja como integrá-lo com um backend Node.js.
Por que Polar.sh? #why-polar-sh
Antes de mergulhar no código, veja por que escolhi Polar.sh em vez de alternativas:
vs Stripe: Polar faz mais pronto para uso. Você obtém páginas de checkout hospedadas, um portal de cliente e gerenciamento de ciclo de vida de assinaturas sem construir isso você mesmo. Stripe dá mais controle mas requer mais código.
vs Paddle/Lemon Squeezy: Benefícios similares de merchant of record (eles lidam com VAT/impostos), mas Polar foi construído especificamente para desenvolvedores e projetos open-source. A DX é notavelmente melhor.
O que Polar faz por você:
- Páginas de checkout hospedadas
- Processamento de pagamentos
- Portal de cliente
- Ciclo de vida de assinaturas
- Conformidade fiscal (como merchant of record)
O que você faz:
- Eventos de webhook
- Direitos do usuário (entitlements)
- Acompanhamento de uso
Configuração do Projeto #project-setup
Instale a SDK e a biblioteca de verificação de webhook:
npm install @polar-sh/sdk standardwebhooks
Inicialize o cliente Polar:
import { Polar } from '@polar-sh/sdk';
export const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN!,
server: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
});
Variáveis de ambiente necessárias:
POLAR_ACCESS_TOKEN=your_access_token
POLAR_WEBHOOK_SECRET=your_webhook_secret
Obtenha esses dados do seu dashboard Polar. Use o modo sandbox durante o desenvolvimento—ele fornece cartões de teste e não cobrar dinheiro real.
Criando Sessões de Checkout #creating-checkout-sessions
Quando um usuário clica em “Assinar” ou “Comprar”, crie uma sessão de checkout e redirecione-o:
import { polar } from '../lib/polar';
export async function createCheckout(user: { id: string; email: string }, productId: string) {
const checkout = await polar.checkouts.create({
products: [productId],
successUrl: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',
customerEmail: user.email,
metadata: {
userId: String(user.id),
},
});
return checkout.url;
}
Pontos importantes:
metadata.userIdvincula o cliente Polar ao seu usuário. Você precisará disso em webhooks.successUrlpode incluir{CHECKOUT_SESSION_ID}que Polar substitui pelo ID real.productsrecebe um array—você pode empacotar múltiplos produtos em um checkout.
Exemplo de rota Express:
router.post('/create-checkout', async (req, res) => {
const user = req.user; // do seu middleware de auth
const { productId } = req.body;
if (!productId) {
return res.status(400).json({ error: 'Product ID required' });
}
try {
const checkoutUrl = await createCheckout(user, productId);
res.json({ url: checkoutUrl });
} catch (error) {
console.error('Checkout creation failed:', error);
res.status(500).json({ error: 'Failed to create checkout' });
}
});
Tratamento de Webhooks #webhook-handling
Webhooks são como Polar informa ao seu app sobre eventos de pagamento. Esta é a parte mais crítica para acertar.
Verificação de Assinatura
Polar usa a especificação Standard Webhooks. Você deve verificar assinaturas para prevenir requisições falsificadas:
import { Webhook } from 'standardwebhooks';
const secret = 'whsec_' + Buffer.from(
process.env.POLAR_WEBHOOK_SECRET!
).toString('base64');
const wh = new Webhook(secret);
export function verifyWebhook(rawBody: string, headers: Record<string, string>) {
try {
return wh.verify(rawBody, headers);
} catch (error) {
console.error('Webhook verification failed:', error);
throw new Error('Invalid webhook signature');
}
}
Configuração da Rota (Crítico!)
A rota de webhook precisa do corpo branco da requisição para verificação de assinatura. Registre-a ANTES do seu middleware JSON:
import express from 'express';
import { webhookRouter } from './routes/webhooks';
const app = express();
// Rotas de webhook PRIMEIRO - precisam do corpo branco
app.use('/api/webhooks', webhookRouter);
// Parsing JSON DEPOIS dos webhooks
app.use(express.json());
// Outras rotas...
app.use('/api', apiRouter);
import { Router } from 'express';
import express from 'express';
import { verifyWebhook } from '../webhooks/polar';
import { handlePolarEvent } from '../services/polar-events';
const router = Router();
router.post('/polar',
express.raw({ type: 'application/json' }),
async (req, res) => {
try {
const event = verifyWebhook(
req.body.toString(),
req.headers as Record<string, string>
);
await handlePolarEvent(event);
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(400).json({ error: 'Webhook processing failed' });
}
}
);
export { router as webhookRouter };
Manipuladores de Eventos
Veja como tratar os principais eventos de assinatura:
import { db } from '../lib/db';
interface PolarEvent {
type: string;
data: {
id: string;
status: string;
customer_id: string;
metadata?: { userId?: string };
current_period_end?: string;
};
}
export async function handlePolarEvent(event: PolarEvent) {
const { type, data } = event;
switch (type) {
case 'subscription.created':
// Vincula cliente Polar ao seu usuário
await db.user.update({
where: { id: data.metadata?.userId },
data: {
polarCustomerId: data.customer_id,
subscriptionId: data.id,
subscriptionStatus: 'pending',
},
});
break;
case 'subscription.active':
// Pagamento bem-sucedido - ativa a assinatura
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: 'active',
currentPeriodEnd: new Date(data.current_period_end!),
// Define limites específicos do plano
apiCallsLimit: 10000,
storageLimit: 5 * 1024 * 1024 * 1024, // 5GB
},
});
break;
case 'subscription.updated':
// Mudança de plano, renovação, etc.
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: data.status,
currentPeriodEnd: new Date(data.current_period_end!),
},
});
break;
case 'subscription.canceled':
// Usuário cancelou - ainda ativo até o fim do período
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: 'canceled',
canceledAt: new Date(),
},
});
break;
case 'subscription.revoked':
// Revogação imediata (pagamento falhou, reembolso, etc.)
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: 'revoked',
apiCallsLimit: 0,
storageLimit: 0,
},
});
break;
case 'order.paid':
// Compra única concluída
await handleOneTimePurchase(data);
break;
default:
console.log(`Unhandled event type: ${type}`);
}
}
async function handleOneTimePurchase(data: PolarEvent['data']) {
// Concede acesso vitalício, créditos, etc.
await db.user.update({
where: { id: data.metadata?.userId },
data: {
lifetimeAccess: true,
},
});
}
Gerenciamento de Assinaturas #subscription-management
Cancelar Assinatura
Permita que usuários cancelem pelo seu app (eles mantêm acesso até o fim do período):
import { polar } from '../lib/polar';
export async function cancelSubscription(subscriptionId: string) {
await polar.subscriptions.update({
id: subscriptionId,
subscriptionUpdate: {
cancelAtPeriodEnd: true,
},
});
}
Portal de Cliente
Polar fornece um portal hospedado onde clientes podem gerenciar faturamento, atualizar métodos de pagamento e ver faturas:
export async function getCustomerPortalUrl(polarCustomerId: string) {
const session = await polar.customerSessions.create({
customerId: polarCustomerId,
});
return session.customerPortalUrl;
}
Rota Express:
router.post('/billing-portal', async (req, res) => {
const user = req.user;
if (!user.polarCustomerId) {
return res.status(400).json({ error: 'No subscription found' });
}
try {
const portalUrl = await getCustomerPortalUrl(user.polarCustomerId);
res.json({ url: portalUrl });
} catch (error) {
console.error('Portal creation failed:', error);
res.status(500).json({ error: 'Failed to create portal session' });
}
});
Armadilhas Comuns #common-pitfalls
Depois de integrar Polar em produção, aqui estão os erros que eu vi (e comi):
1. Middleware JSON antes dos webhooks
Se você vê erros de verificação de assinatura, esta é quase sempre a causa. O corpo branco é analisado por express.json() antes que seu manipulador de webhook o veja.
Correção: Registre rotas de webhook antes de express.json().
2. Não tratar todos os estados de assinatura
Uma assinatura pode ser: active, canceled, past_due, incomplete, trialing, ou revoked. Não verifique apenas active—trate o ciclo de vida completo.
3. Metadados faltando
Se você esquecer de passar userId nos metadados do checkout, não pode vincular o cliente Polar ao seu usuário. Sempre inclua informações identificadoras.
4. Manipuladores de webhook não idempotentes
Polar pode tentar novamente webhooks. Seus manipuladores devem ser idempotentes—processar o mesmo evento duas vezes não deve quebrar nada.
// Ruim - cria registros duplicados
await db.payment.create({ data: { orderId: event.data.id, ... } });
// Bom - upsert baseado em ID único
await db.payment.upsert({
where: { polarOrderId: event.data.id },
create: { polarOrderId: event.data.id, ... },
update: { ... },
});
5. Respostas de webhook bloqueantes
Endpoints de webhook devem responder rapidamente. Se precisar fazer processamento pesado, confirme imediatamente e processe assincronamente:
router.post('/polar', express.raw({ type: 'application/json' }), async (req, res) => {
const event = verifyWebhook(req.body.toString(), req.headers);
// Responde imediatamente
res.status(200).json({ received: true });
// Processa assincronamente (use uma fila de jobs adequada em produção)
setImmediate(() => handlePolarEvent(event));
});
Testando sua Integração #testing-your-integration
Use o Modo Sandbox
O ambiente sandbox da Polar permite testar o fluxo completo sem cobranças reais:
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: 'sandbox', // Modo de teste
});
Cartões de Teste
No sandbox, use estes números de cartão de teste:
4242 4242 4242 4242- Pagamento bem-sucedido4000 0000 0000 0002- Cartão recusado
Teste Local de Webhook
Use ngrok ou similar para expor seu servidor local:
ngrok http 3000
Então defina sua URL de webhook no dashboard Polar para: https://your-ngrok-url.ngrok.io/api/webhooks/polar
Verifique sua Configuração
Checklist antes de ir para produção:
- Checkout cria com sucesso e redireciona para Polar
- URL de sucesso redireciona de volta para seu app
- Verificação de assinatura de webhook passa
- Evento
subscription.activeativa o acesso do usuário - Portal de cliente carrega corretamente
- Cancelamento funciona e define
cancelAtPeriodEnd
Esta é a integração completa de pagamentos com Polar.sh. A visão principal: deixe Polar fazer o que faz bem (UI de checkout, processamento de pagamentos, portal de cliente) e foque seu código no tratamento de webhooks e direitos do usuário.
Para mais detalhes, confira a documentação oficial da Polar.