Blog Recursos Sobre Pesquisar Tópicos
Tutorials

Pagamentos com Polar.sh em Node.js: Guia Completo de Backend

Atualizado em 3 de janeiro de 2026

Categoria: Tutorials
Compartilhar

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:

Terminal
npm install @polar-sh/sdk standardwebhooks

Inicialize o cliente Polar:

src/lib/polar.ts
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:

.env
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:

src/routes/checkout.ts
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.userId vincula o cliente Polar ao seu usuário. Você precisará disso em webhooks.
  • successUrl pode incluir {CHECKOUT_SESSION_ID} que Polar substitui pelo ID real.
  • products recebe um array—você pode empacotar múltiplos produtos em um checkout.

Exemplo de rota Express:

src/routes/api.ts
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:

src/webhooks/polar.ts
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:

src/app.ts
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);
src/routes/webhooks.ts
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:

src/services/polar-events.ts
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):

src/services/subscriptions.ts
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:

src/services/subscriptions.ts
export async function getCustomerPortalUrl(polarCustomerId: string) {
    const session = await polar.customerSessions.create({
        customerId: polarCustomerId,
    });

    return session.customerPortalUrl;
}

Rota Express:

src/routes/api.ts
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-sucedido
  • 4000 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.active ativa 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.

Categoria Tutorials
Compartilhar

Receba os últimos insights de IA entregues na sua caixa de entrada

Mantenha-se atualizado com as últimas tendências, tutoriais e insights da indústria. Junte-se à comunidade de desenvolvedores que confiam em nosso boletim.

Apenas contas novas. Ao enviar seu email, você concorda com nossa Política de Privacidade