🌐 Detecting your location…

Como adicionar Stripe Payments a um aplicativo Next.js em 2026: guia completo de integração

⏱️8 min read  ·  1,552 words

A maioria das integrações do Stripe falha no mesmo lugar: eles tratam o redirecionamento do navegador como prova de pagamento. Não é. A única fonte confiável da verdade é o webhook que o Stripe envia ao seu servidor. Este guia constrói uma integração correta em Next.js com o App Router — Checkout, webhooks com verificação de assinatura, atendimento idempotente e assinaturas.

Arquitetura em primeiro lugar

Compreender o fluxo evita os dois bugs que quebram a maioria das integrações.

  1. Seu servidor cria uma sessão de checkout e retorna sua URL.
  2. O navegador redireciona para Stripe. O usuário paga na página do Stripe – os detalhes do cartão nunca tocam no seu servidor.
  3. Stripe redireciona o navegador de volta para o seu URL de sucesso.
  4. Separadamente, o Stripe envia um webhook ao seu servidor confirmando o pagamento.
  5. Seu manipulador de webhook concede acesso. Não é a página de sucesso.

A etapa 5 é o jogo inteiro. O usuário pode fechar a aba antes do redirecionamento, perder a conectividade ou simplesmente editar a URL de sucesso e visitá-la diretamente. Conceda acesso apenas no webhook.

Etapa 1: Instalar e configurar

npm install stripe @stripe/stripe-js

Armazene chaves em variáveis de ambiente. A chave secreta nunca deve chegar ao cliente, portanto não recebeNEXT_PUBLIC_ prefixo.

# .env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_BASE_URL=http://localhost:3000

Crie um único cliente Stripe compartilhado para não construir um por solicitação.

// lib/stripe.ts
import Stripe from 'stripe';

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error('STRIPE_SECRET_KEY is not set');
}

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2026-06-30',
  typescript: true,
});

FixandoapiVersion assuntos. Sem ele, você herda qualquer versão padrão da sua conta, e isso pode mudar sob você.

Etapa 2: Crie a sessão de checkout

Nunca aceite um preço do cliente. Envie um identificador de produto e consulte o preço no servidor, ou o usuário simplesmente edita a solicitação e paga um centavo.

// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { getCurrentUser } from '@/lib/auth';

const PRICES: Record<string, string> = {
  pro_monthly: 'price_1AbCdEfGhIjKlMnO',
  pro_yearly:  'price_1XyZaBcDeFgHiJkL',
};

export async function POST(req: Request) {
  const user = await getCurrentUser();
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { plan } = await req.json();
  const priceId = PRICES[plan];
  if (!priceId) {
    return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
  }

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/welcome?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url:  `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
    customer_email: user.email,
    // client_reference_id survives the round trip and arrives in the webhook.
    client_reference_id: user.id,
    metadata: { userId: user.id, plan },
  });

  return NextResponse.json({ url: session.url });
}

client_reference_id emetadata são como você sabe qual usuário pagou quando o webhook chega. Omita-os e você estará combinando os pagamentos às contas por e-mail, o que é interrompido no momento em que alguém paga com um endereço diferente.

Etapa 3: Redirecionar do cliente

'use client';

export function UpgradeButton({ plan }: { plan: string }) {
  async function handleClick() {
    const res = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ plan }),
    });

    if (!res.ok) {
      alert('Could not start checkout. Please try again.');
      return;
    }

    const { url } = await res.json();
    window.location.href = url;
  }

  return <button onClick={handleClick}>Upgrade</button>;
}

Etapa 4: O Webhook — Onde mora a correção

Duas coisas quebram os webhooks no Next.js. Primeiro, você deve verificar a assinatura em relação aocru corpo da solicitação; se alguma coisa analisar primeiro em JSON, a verificação falhará. Em segundo lugar, você deve lidar com entregas duplicadas, porque o Stripe tentará novamente e entregará o mesmo evento mais de uma vez.

// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { grantAccess, revokeAccess, hasProcessed, markProcessed } from '@/lib/billing';

export async function POST(req: Request) {
  // req.text() gives the raw body, which signature verification requires.
  const body = await req.text();
  const signature = (await headers()).get('stripe-signature');

  if (!signature) {
    return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
  }

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Signature verification failed:', err);
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Stripe retries on any non-2xx, so the same event can arrive repeatedly.
  if (await hasProcessed(event.id)) {
    return NextResponse.json({ received: true });
  }

  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object;
        const userId = session.metadata?.userId ?? session.client_reference_id;
        if (userId) {
          await grantAccess(userId, {
            customerId: session.customer as string,
            subscriptionId: session.subscription as string,
          });
        }
        break;
      }

      case 'customer.subscription.deleted': {
        await revokeAccess(event.data.object.customer as string);
        break;
      }

      case 'invoice.payment_failed': {
        // Notify the user; do not revoke immediately — cards fail temporarily.
        break;
      }
    }

    await markProcessed(event.id);
    return NextResponse.json({ received: true });
  } catch (err) {
    console.error('Webhook handler failed:', err);
    // Return 500 so Stripe retries rather than dropping the event.
    return NextResponse.json({ error: 'Handler failed' }, { status: 500 });
  }
}

hasProcessed emarkProcessed deve gravar em uma tabela digitada no ID do evento Stripe com uma restrição exclusiva. Essa única restrição é o que torna o cumprimento idempotente – sem ela, uma nova tentativa credita duas vezes a conta.

Etapa 5: testar webhooks localmente

Stripe não pode alcançarlocalhost, então encaminhe eventos com a CLI.

stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# In another terminal, fire a test event:
stripe trigger checkout.session.completed

stripe listen imprime um segredo de assinatura do webhook. Use esse valor paraSTRIPE_WEBHOOK_SECRET em desenvolvimento — é diferente daquele no Dashboard.

Etapa 6: A página de sucesso confirma, mas não concede

A página de sucesso deve indicar o estado que seu webhook já escreveu. Se o webhook ainda não tiver chegado, mostre um estado pendente em vez de conceder qualquer coisa.

// app/welcome/page.tsx
import { getCurrentUser } from '@/lib/auth';
import { getSubscription } from '@/lib/billing';

export default async function WelcomePage() {
  const user = await getCurrentUser();
  const subscription = await getSubscription(user.id);

  if (!subscription?.active) {
    return (
      <p>Payment received. Your account is being activated — this usually takes a few seconds.</p>
    );
  }

  return <h1>Welcome to Pro</h1>;
}

Etapa 7: Permita que os clientes gerenciem suas próprias assinaturas

O Portal de Cobrança trata de alterações de planos, cancelamentos, faturas e atualizações de cartões. Construir você mesmo esses fluxos são semanas de trabalho que você não precisa fazer.

// app/api/portal/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { getCurrentUser } from '@/lib/auth';
import { getCustomerId } from '@/lib/billing';

export async function POST() {
  const user = await getCurrentUser();
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  const customerId = await getCustomerId(user.id);
  if (!customerId) return NextResponse.json({ error: 'No subscription' }, { status: 400 });

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/account`,
  });

  return NextResponse.json({ url: session.url });
}

Lista de verificação de ativação

  • Troque chaves de teste por chaves ativas e crie umseparar endpoint webhook em modo ao vivo com seu próprio segredo de assinatura
  • Confirme se o URL do seu webhook está acessível publicamente e retorna 2xx rapidamente – faça um trabalho lento de forma assíncrona
  • Ative o Stripe Tax se você vende em várias jurisdições
  • Ative as regras do Radar para proteção contra fraudes
  • Teste o fluxo completo com um cartão real e depois faça o reembolso
  • Verifique se sua tabela de idempotência possui um índice exclusivo no ID do evento

Erros Comuns

Concedendo acesso na página de sucesso. Os usuários que nunca o acessam ainda pagam, e os usuários que o visitam diretamente nunca o fazem.

Analisando o corpo antes da verificação da assinatura. A verificação precisa dos bytes brutos. Usarreq.text() e nada mais primeiro.

Confiar em um preço do cliente. Procure preços no servidor por identificador, sempre.

Retornando 200 em caso de falha do manipulador. Stripe trata 2xx como sucesso e para de tentar novamente, então o evento é perdido permanentemente. Retorne 500 e deixe tentar novamente.

Revogando o acesso no primeiro pagamento com falha. Os cartões falham por motivos temporários. Em vez disso, siga o processo de cobrança do Stripe.

Conclusão

Uma integração correta do Stripe depende de algumas regras:crie sessões no lado do servidor com preços que você controla, trate o webhook como a única fonte de verdade, verifique assinaturas em relação ao corpo bruto, torne o cumprimento idempotente com uma restrição exclusiva no ID do evento e retorne não-2xx quando o tratamento falhar, então Stripe tenta novamente. Faça o gerenciamento de assinaturas manualmente no Portal de cobrança, em vez de reconstruí-lo. Faça tudo certo e os pagamentos se tornarão a parte menos agitada do seu aplicativo.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work — which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা