💳 Stripe Gateway: Do Zero ao Webhook
Integre o gateway de pagamento mais confiável do mundo. Aprenda a processar pagamentos, gerenciar assinaturas e implementar webhooks para sincronizar dados automaticamente.
🎯 Por que isso é importante
Stripe processa mais de $640 bilhões anualmente e é usado por 85% das Fortune 500. Dominar esta integração significa criar sistemas de pagamento seguros, escaláveis e confiáveis que geram receita real para negócios.
🧠 Conceitos Fundamentais
Payment Intent
Objeto que representa a intenção de coletar pagamento do cliente. Controla todo o fluxo de autorização e captura do pagamento.
Webhooks
Notificações HTTP que o Stripe envia para sua aplicação quando eventos importantes acontecem (pagamento aprovado, falha, etc.).
Customer Portal
Interface pronta do Stripe onde clientes gerenciam assinaturas, métodos de pagamento e faturas sem código adicional.
PCI Compliance
Stripe Elements garante conformidade PCI automaticamente, protegendo dados sensíveis sem complexidade adicional.
🛠️ Implementação Completa
Configuração Inicial
1. Instalação das Dependências
# Instalar Stripe SDK oficial
npm install stripe @stripe/stripe-js 2. Conta Stripe Dashboard
• Crie conta gratuita em stripe.com
• Acesse o Dashboard → Developers → API Keys
• Copie as chaves: Publishable Key e Secret Key
• Configure webhook endpoint para receber eventos
Variáveis de Ambiente
# .env.local
# 🔑 Stripe Keys (obrigatórias)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
# 🔐 Webhook Secret (para validar eventos)
STRIPE_WEBHOOK_SECRET=whsec_...
# 🌐 URL base da aplicação
NEXT_PUBLIC_APP_URL=http://localhost:3000
# 📊 Stripe Products (IDs dos produtos criados no Dashboard)
STRIPE_PRICE_ID_BASIC=price_...
STRIPE_PRICE_ID_PRO=price_...⚠️ Segurança Crítica
NUNCA exponha STRIPE_SECRET_KEY no cliente. Use apenas em API Routes server-side. A chave publishable é segura para uso público.
💰 Exemplo Prático: Tela de Pricing
Componente de Pricing Completo
Vamos criar uma tela de pricing profissional que se conecta diretamente ao Stripe. Este exemplo mostra como implementar planos de assinatura com checkout automático.
'use client';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Check } from 'lucide-react';
import { features } from 'process';
import { useState } from 'react';
const plans = [
{
name: 'Básico',
price: '$ 29,90',
priceId: 'STRIPE_PRICE_ID_BASIC',
description: 'Para começar',
features: [
'Acesso a plataforma',
'Suporte básico',
'Atualizações regulares',
],
},
{
name: 'Pro',
price: '$ 39,90',
priceId: 'STRIPE_PRICE_ID_PRO',
description:
'Para usuários avançados',
features: [
'Acesso a plataforma',
'Suporte básico',
'Atualizações regulares',
],
popular: true,
},
];
export default function PricingPage() {
const [loading, setLoading] =
useState<string | null>(null);
const handleSubscribe = async (
priceId: string,
) => {
setLoading(priceId);
try {
const response = await fetch(
'/api/create-checkout-session',
{
method: 'POST',
headers: {
'Content-Type':
'application/json',
},
body: JSON.stringify({
priceId,
}),
},
);
const data =
await response.json();
if (data.url) {
window.location.href = data.url;
} else {
console.error(
'Erro ao criar sessão:',
data.error,
);
alert(
'Erro ao processar pagamento. Tente novamente.',
);
}
} catch (error) {
console.error(
'Error during subscription:',
error,
);
setLoading(null);
}
};
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-16">
<div className="text-center mb-12">
<h1 className="text-5xl font-bold mb-4">
Escolha seu plano
</h1>
<p className="text-xl text-muted-foreground">
Compare os recursos e
escolha o plano que melhor
atende às suas necessidades.
</p>
</div>
<div className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto">
{plans.map((plan) => (
<Card
key={plan.name}
className={`relative ${
plan.popular
? 'border-2 border-primary shadow-lg'
: ''
}`}
>
{plan.popular && (
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
<span className="bg-primary text-foreground px-4 py-1 rounded-full text-sm font-medium">
Mais popular
</span>
</div>
)}
<CardHeader className="text-center">
<CardTitle className="text-2xl">
{plan.name}
</CardTitle>
<CardDescription>
{plan.description}
</CardDescription>
<div className="mt-4">
<span className="text-4xl font-bold text-muted-foreground">
{plan.price}
</span>
<span className="text-muted-foreground">
/mês
</span>
</div>
</CardHeader>
<CardContent>
<ul className="space-y-3">
{plan.features.map(
(feature) => (
<li
key={feature}
className="flex items-center"
>
<Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-muted-foreground">
{feature}
</span>
</li>
),
)}
</ul>
</CardContent>
<CardFooter>
<Button
onClick={() => {
handleSubscribe(
plan.priceId,
);
}}
disabled={
loading ===
plan.priceId
}
className={`w-full cursor-pointer`}
>
{loading ===
plan.priceId
? 'Processando...'
: `Assinar ${plan.name}`}
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
</div>
);
}🔌 API Route: Checkout Session
Criando Sessão de Checkout
Esta API route cria uma sessão segura de checkout no Stripe e redireciona o usuário para a página de pagamento oficial.
// app/api/create-checkout-session/route.ts
import { NextResponse } from 'next/server';
import {
auth,
currentUser,
} from '@clerk/nextjs/server';
import Stripe from 'stripe';
const stripeSecretKey =
process.env.STRIPE_SECRET_KEY;
if (
!stripeSecretKey ||
stripeSecretKey.length < 25
) {
console.error(
'⚠️ STRIPE_SECRET_KEY ausente ou inválida',
);
throw new Error(
'Stripe não configurado. Defina STRIPE_SECRET_KEY nas variáveis de ambiente.',
);
}
const stripe = new Stripe(
stripeSecretKey,
{ apiVersion: '2025-07-30.basil' },
);
export async function POST(
request: Request,
) {
try {
const { userId } = await auth();
const user = await currentUser();
if (!userId || !user) {
return new NextResponse(
'Unauthorized',
{ status: 401 },
);
}
const { quantity = 1, priceId } =
await request.json();
if (!priceId) {
return new NextResponse(
'Price ID is required',
{ status: 400 },
);
}
const session =
await stripe.checkout.sessions.create(
{
line_items: [
{
price:
process.env[priceId],
quantity,
},
],
mode: 'subscription',
success_url: `${request.headers.get(
'origin',
)}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.headers.get(
'origin',
)}/pricing`,
client_reference_id: userId,
customer_email:
user.primaryEmailAddress
?.emailAddress,
metadata: {
clerkUserId: userId,
userEmail:
user.primaryEmailAddress
?.emailAddress || '',
},
},
);
return NextResponse.json({
url: session.url,
});
} catch (error) {
console.error(
'Erro ao obter usuário autenticado:',
error,
);
return new NextResponse(
'Internal error',
{ status: 500 },
);
}
}🗄️ Modelo do Banco de Dados
Schema Prisma para Assinaturas
Primeiro, vamos definir o modelo de dados para armazenar as informações das assinaturas do Stripe. Este modelo será usado para sincronizar os dados entre o Stripe e nosso banco de dados.
// prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Subscription {
id String @id @default(cuid())
clerkUserId String @unique
stripeCustomerId String @unique
stripeSubscriptionId String @unique
status String // active, canceled, past_due, etc.
priceId String
cancelAtPeriodEnd Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("subscriptions")
}📋 Campos do Modelo:
clerkUserId- ID único do usuário no ClerkstripeCustomerId- ID do cliente no StripestripeSubscriptionId- ID da assinatura no Stripestatus- Status da assinatura (active, canceled, etc.)priceId- ID do preço/plano no StripecancelAtPeriodEnd- Se deve cancelar no fim do período
⚡ Comandos Prisma:
🔧 Serviços de Assinatura
Gerenciamento de Assinaturas
Primeiro, vamos criar os serviços para gerenciar assinaturas no banco de dados. Estas funções serão utilizadas pelos webhooks para sincronizar dados.
// lib/services/subscription.ts
// lib/services/subscription.ts
import { prisma } from '@/lib/prisma';
import { Subscription as PrismaSubscription } from '@prisma/client';
import Stripe from 'stripe';
export interface CreateSubscriptionData {
clerkUserId: string;
stripeCustomerId: string;
stripeSubscriptionId: string;
status: string;
priceId: string;
cancelAtPeriodEnd?: boolean;
}
export async function createSubscription(
data: CreateSubscriptionData,
): Promise<PrismaSubscription> {
return await prisma.subscription.create(
{
data,
},
);
}
export async function updateSubscriptionByStripeId(
stripeSubscriptionId: string,
data: Partial<CreateSubscriptionData>,
): Promise<PrismaSubscription | null> {
return await prisma.subscription.update(
{
where: {
stripeSubscriptionId,
},
data,
},
);
}
export async function getSubscriptionByClerkId(
clerkUserId: string,
): Promise<PrismaSubscription | null> {
return await prisma.subscription.findUnique(
{
where: {
clerkUserId,
},
},
);
}
export async function getSubscriptionByStripeId(
stripeSubscriptionId: string,
): Promise<PrismaSubscription | null> {
return await prisma.subscription.findUnique(
{
where: {
stripeSubscriptionId,
},
},
);
}
export async function cancelSubscription(
stripeSubscriptionId: string,
): Promise<PrismaSubscription | null> {
try {
const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY!,
{
apiVersion: '2025-07-30.basil',
},
);
await stripe.subscriptions.cancel(
stripeSubscriptionId,
);
} catch (error) {
console.error(
'Erro ao cancelar assinatura:',
error,
);
throw error;
}
return await prisma.subscription.update(
{
where: {
stripeSubscriptionId,
},
data: {
status: 'canceled',
cancelAtPeriodEnd: true,
},
},
);
}
export async function createOrUpdateSubscriptionFromStripe(
stripeSubscription: Stripe.Subscription,
clerkUserId?: string,
): Promise<PrismaSubscription> {
// Extract only the essential fields from Stripe.Subscription
const subscriptionData = {
stripeCustomerId:
stripeSubscription.customer as string,
stripeSubscriptionId:
stripeSubscription.id,
status: stripeSubscription.status,
priceId:
stripeSubscription.items.data[0]
?.price.id || '',
cancelAtPeriodEnd:
stripeSubscription.cancel_at_period_end,
};
// Try to find existing subscription
const existingSubscription =
await getSubscriptionByStripeId(
stripeSubscription.id,
);
if (existingSubscription) {
// Update existing subscription
const updatedSubscription =
await updateSubscriptionByStripeId(
stripeSubscription.id,
subscriptionData,
);
if (!updatedSubscription) {
throw new Error(
'Falha ao atualizar assinatura existente',
);
}
return updatedSubscription;
} else {
// Create new subscription - clerkUserId is required for new subscriptions
if (!clerkUserId) {
throw new Error(
'clerkUserId é obrigatório para criar nova assinatura',
);
}
return await createSubscription({
clerkUserId,
...subscriptionData,
});
}
}📡 API para Consultar Assinatura
Endpoint para Assinatura Atual
Esta API permite que o frontend consulte o status da assinatura do usuário logado, verificando se possui uma assinatura ativa e seus detalhes.
// app/api/subscriptions/current/route.ts
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { getSubscriptionByClerkId } from "@/lib/services/subscription";
export async function GET() {
try {
const { userId } = await auth();
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
const subscription = await getSubscriptionByClerkId(userId);
if (!subscription) {
return NextResponse.json({
subscription: null,
hasActiveSubscription: false,
});
}
// Check if subscription is active
const isActive = subscription.status === "active";
return NextResponse.json({
subscription: {
id: subscription.id,
status: subscription.status,
priceId: subscription.priceId,
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
createdAt: subscription.createdAt,
updatedAt: subscription.updatedAt,
customerId: subscription.stripeCustomerId,
},
hasActiveSubscription: isActive,
});
} catch (error) {
console.error("Error fetching subscription:", error);
return new NextResponse("Internal error", { status: 500 });
}
}✅ Funcionalidades:
- • Autenticação via Clerk
- • Busca assinatura por ID do usuário
- • Verifica se a assinatura está ativa
- • Retorna dados sanitizados da assinatura
- • Tratamento de erros completo
📋 Exemplo de Uso no Frontend:
const response = await fetch('/api/subscriptions/current');
const { subscription, hasActiveSubscription } = await response.json();🏠 Tela de Dashboard
Dashboard do Usuário
Implementação completa de uma tela de dashboard que consome a API de assinatura e exibe informações detalhadas sobre o status da assinatura do usuário.
'use client';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Settings,
CheckCircle,
Clock,
AlertCircle,
} from 'lucide-react';
import { useSearchParams } from 'next/navigation';
import {
Suspense,
useEffect,
useState,
} from 'react';
interface SubscriptionData {
subscription: {
id: string;
status: string;
priceId: string;
cancelAtPeriodEnd: boolean;
createdAt: string;
updatedAt: string;
customerId: string;
} | null;
hasActiveSubscription: boolean;
}
function DashboardContent() {
const searchParams =
useSearchParams();
const sessionId = searchParams.get(
'session_id',
);
const [
subscriptionData,
setSubscriptionData,
] = useState<SubscriptionData | null>(
null,
);
const [loading, setLoading] =
useState(true);
const [error, setError] = useState<
string | null
>(null);
useEffect(() => {
const fetchSubscription =
async () => {
try {
const response = await fetch(
'/api/subscriptions/current',
);
if (!response.ok) {
throw new Error(
'Failed to fetch subscription',
);
}
const data =
await response.json();
setSubscriptionData(data);
} catch (err) {
setError(
'Erro ao carregar dados da assinatura',
);
console.error(
'Error fetching subscription:',
err,
);
} finally {
setLoading(false);
}
};
fetchSubscription();
}, []);
const getSubscriptionStatus = () => {
if (!subscriptionData?.subscription)
return {
text: 'Sem assinatura',
variant: 'secondary' as const,
icon: AlertCircle,
};
const {
status,
cancelAtPeriodEnd,
} = subscriptionData.subscription;
if (
status === 'active' &&
!cancelAtPeriodEnd
) {
return {
text: 'Plano Pro',
variant: 'default' as const,
icon: CheckCircle,
};
} else if (
status === 'active' &&
cancelAtPeriodEnd
) {
return {
text: 'Cancelando',
variant: 'destructive' as const,
icon: Clock,
};
} else if (status === 'past_due') {
return {
text: 'Pagamento pendente',
variant: 'destructive' as const,
icon: AlertCircle,
};
} else {
return {
text: 'Inativo',
variant: 'secondary' as const,
icon: AlertCircle,
};
}
};
const formatDate = (
dateString: string,
) => {
return new Date(
dateString,
).toLocaleDateString('pt-BR');
};
const statusInfo =
getSubscriptionStatus();
const StatusIcon = statusInfo.icon;
if (loading) {
return (
<div className="min-h-screen bg-bg-2 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-border mx-auto mb-4"></div>
<p>Carregando...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-bg-2">
<div className="container mx-auto px-4 py-8">
{/* Success Message */}
{sessionId && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-3" />
<div>
<p className="text-green-800 font-medium">
Assinatura ativada com
sucesso!
</p>
<p className="text-green-600 text-sm">
Bem-vindo ao MinimalSaaS
</p>
</div>
</div>
)}
{/* Welcome Section */}
<div className="mb-8">
<h2 className="text-3xl font-bold mb-2">
Bem-vindo de volta!
</h2>
<p className="text-fg-dim">
Aqui está o resumo da sua
conta
</p>
</div>
{/* Subscription Details */}
{subscriptionData?.subscription && (
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h3 className="text-xl font-semibold mb-4">
Detalhes da Assinatura
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-fg-dim">
Status
</p>
<p className="font-medium capitalize">
{
subscriptionData
.subscription
.status
}
</p>
</div>
<div>
<p className="text-sm text-fg-dim">
Cancelamento
automático
</p>
<p className="font-medium">
{subscriptionData
.subscription
.cancelAtPeriodEnd
? 'Sim'
: 'Não'}
</p>
</div>
<div>
<p className="text-sm text-fg-dim">
Criada em
</p>
<p className="font-medium">
{formatDate(
subscriptionData
.subscription
.createdAt,
)}
</p>
</div>
<div>
<p className="text-sm text-fg-dim">
Última atualização
</p>
<p className="font-medium">
{formatDate(
subscriptionData
.subscription
.updatedAt,
)}
</p>
</div>
<CustomerPortalButton
customerId={
subscriptionData
?.subscription
?.customerId
}
/>
</div>
</div>
)}
{/* No Subscription Message */}
{!subscriptionData?.subscription &&
!loading && (
<div className="bg-white rounded-lg shadow p-6 text-center">
<AlertCircle className="h-12 w-12 text-fg-muted mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">
Nenhuma assinatura ativa
</h3>
<p className="text-fg-dim mb-4">
Você não possui uma
assinatura ativa no
momento.
</p>
<Button asChild>
<a href="/pricing">
Ver Planos
</a>
</Button>
</div>
)}
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800">
{error}
</p>
</div>
)}
</div>
</div>
);
}
export default function DashboardPage() {
return (
<Suspense
fallback={
<div>Carregando...</div>
}
>
<DashboardContent />
</Suspense>
);
}
function CustomerPortalButton({
customerId,
}: {
customerId: string;
}) {
const [loading, setLoading] =
useState(false);
const handlePortalAccess =
async () => {
setLoading(true);
try {
const response = await fetch(
'/api/stripe/create-portal-session',
{
method: 'POST',
headers: {
'Content-Type':
'application/json',
},
body: JSON.stringify({
customerId,
}),
},
);
const { url } =
await response.json();
window.location.href = url;
} catch (error) {
console.error('Erro:', error);
} finally {
setLoading(false);
}
};
return (
<Button
onClick={handlePortalAccess}
disabled={loading}
variant="outline"
className="border-border"
>
{loading
? 'Carregando...'
: '⚙️ Gerenciar Assinatura'}
</Button>
);
}
🎯 Funcionalidades do Dashboard:
- • Estado de Loading: Spinner durante carregamento dos dados
- • Mensagem de Sucesso: Exibe confirmação após checkout bem-sucedido
- • Status da Assinatura: Badges dinâmicos baseados no status
- • Detalhes Completos: Informações formatadas da assinatura
- • Portal do Cliente: Botão para gerenciar assinatura no Stripe
- • Estado Vazio: Mensagem quando não há assinatura ativa
- • Tratamento de Erros: Feedback visual para problemas de API
🔄 Fluxo de Estados:
1. Loading: Exibe spinner enquanto busca dados da API
2. Success: Mostra dados da assinatura se encontrada
3. Empty: Exibe call-to-action para criar assinatura
4. Error: Mostra mensagem de erro em caso de falha
📱 Recursos de UX:
🔔 Webhook: Sincronização Automática
Processamento de Eventos
Webhooks garantem que sua aplicação seja notificada instantaneamente sobre mudanças no status de pagamentos, assinaturas e outros eventos importantes.
// app/api/webhooks/stripe/route.ts
import {
type NextRequest,
NextResponse,
} from 'next/server';
import Stripe from 'stripe';
import {
createOrUpdateSubscriptionFromStripe,
cancelSubscription,
} from '@/lib/services/subscription';
const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY!,
{
apiVersion: '2025-07-30.basil',
},
);
const webhookSecret =
process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(
request: NextRequest,
) {
try {
const body = await request.text();
const signature =
request.headers.get(
'stripe-signature',
)!;
let event: Stripe.Event;
try {
event =
stripe.webhooks.constructEvent(
body,
signature,
webhookSecret,
);
} catch (err) {
console.error(
'Webhook signature verification failed:',
err,
);
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 },
);
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed':
const session = event.data
.object as Stripe.Checkout.Session;
console.log(
'Payment successful:',
session.id,
);
try {
const clerkUserId =
session.client_reference_id;
if (!clerkUserId) {
console.error(
'No clerk user ID found in session',
);
break;
}
// Get the subscription from Stripe
if (session.subscription) {
const subscription =
await stripe.subscriptions.retrieve(
session.subscription as string,
);
await createOrUpdateSubscriptionFromStripe(
subscription,
clerkUserId,
);
console.log(
'Subscription saved to database:',
subscription.id,
);
}
} catch (error) {
console.error(
'Error handling checkout session:',
error,
);
}
break;
case 'customer.subscription.created':
const subscription = event.data
.object as Stripe.Subscription;
console.log(
'Subscription created:',
subscription.id,
);
try {
// Try to get clerkUserId from customer metadata
const customer =
await stripe.customers.retrieve(
subscription.customer as string,
);
const clerkUserId = (
customer as Stripe.Customer
).metadata?.clerkUserId;
await createOrUpdateSubscriptionFromStripe(
subscription,
clerkUserId,
);
console.log(
'New subscription saved to database:',
subscription.id,
);
} catch (error) {
console.error(
'Error handling subscription creation:',
error,
);
}
break;
case 'customer.subscription.updated':
const updatedSubscription =
event.data
.object as Stripe.Subscription;
console.log(
'Subscription updated:',
updatedSubscription.id,
);
const subscriptionUpdated = {
...updatedSubscription,
status:
updatedSubscription?.cancellation_details
? 'canceled'
: updatedSubscription.status,
};
try {
await createOrUpdateSubscriptionFromStripe(
subscriptionUpdated,
);
console.log(
'Subscription updated in database:',
subscriptionUpdated.id,
);
} catch (error) {
console.error(
'Error handling subscription update:',
error,
);
}
break;
case 'customer.subscription.deleted':
const deletedSubscription =
event.data
.object as Stripe.Subscription;
console.log(
'Subscription cancelled:',
deletedSubscription.id,
);
try {
await cancelSubscription(
deletedSubscription.id,
);
console.log(
'Subscription cancelled in database:',
deletedSubscription.id,
);
} catch (error) {
console.error(
'Error handling subscription cancellation:',
error,
);
}
break;
case 'invoice.payment_succeeded':
const invoice = event.data
.object as Stripe.Invoice;
console.log(
'Payment succeeded:',
invoice.id,
);
try {
// Get subscription ID from the invoice line items
const subscriptionId =
invoice.lines?.data?.[0]
?.subscription;
if (
subscriptionId &&
typeof subscriptionId ===
'string'
) {
const subscription =
await stripe.subscriptions.retrieve(
subscriptionId,
);
await createOrUpdateSubscriptionFromStripe(
subscription,
);
console.log(
'Subscription updated after successful payment:',
subscription.id,
);
}
} catch (error) {
console.error(
'Error handling successful payment:',
error,
);
}
break;
case 'invoice.payment_failed':
const failedInvoice = event.data
.object as Stripe.Invoice;
console.log(
'Payment failed:',
failedInvoice.id,
);
try {
// Get subscription ID from the invoice line items
const subscriptionId =
failedInvoice.lines
?.data?.[0]?.subscription;
if (
subscriptionId &&
typeof subscriptionId ===
'string'
) {
const subscription =
await stripe.subscriptions.retrieve(
subscriptionId,
);
await createOrUpdateSubscriptionFromStripe(
subscription,
);
console.log(
'Subscription updated after failed payment:',
subscription.id,
);
}
} catch (error) {
console.error(
'Error handling failed payment:',
error,
);
}
break;
default:
console.log(
`Unhandled event type: ${event.type}`,
);
}
return NextResponse.json({
received: true,
});
} catch (error) {
console.error(
'Webhook error:',
error,
);
return NextResponse.json(
{
error: 'Webhook handler failed',
},
{ status: 500 },
);
}
}👤 Customer Portal: Autoatendimento
Portal do Cliente
O Customer Portal do Stripe oferece uma interface completa onde clientes podem gerenciar assinaturas, métodos de pagamento e faturas sem código adicional.
// app/api/create-portal-session/route.ts
// app/api/create-portal-session/route.ts
import {
NextRequest,
NextResponse,
} from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY!,
{
apiVersion: '2025-07-30.basil',
},
);
export async function POST(
request: NextRequest,
) {
try {
const { customerId } =
await request.json();
if (!customerId) {
return NextResponse.json(
{
error:
'Customer ID é obrigatório',
},
{ status: 400 },
);
}
// Criar sessão do portal do cliente
const portalSession =
await stripe.billingPortal.sessions.create(
{
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
},
);
return NextResponse.json({
url: portalSession.url,
});
} catch (error) {
console.error(
'Erro ao criar portal session:',
error,
);
return NextResponse.json(
{
error:
'Erro interno do servidor',
},
{ status: 500 },
);
}
}✅ Vantagens desta Implementação
Segurança Máxima
- • PCI Compliance automático - Stripe cuida da certificação
- • Webhook verification - Eventos validados criptograficamente
- • 3D Secure nativo - Proteção contra fraudes
Performance
- • API ultra-rápida - 99.99% uptime garantido
- • Checkout otimizado - Conversão 30% maior
- • Retry automático - Falhas temporárias tratadas
Escala Global
- • 135+ moedas - Vendas internacionais
- • Métodos locais - PIX, boleto, cartões
- • Multi-idioma - Experiência localizada
Automação Total
- • Webhooks confiáveis - Sincronização automática
- • Customer Portal - Autoatendimento completo
- • Retry inteligente - Recuperação de falhas
🧪 Testando a Implementação
Validação Completa
Antes de ir para produção, é essencial testar todos os fluxos de pagamento usando as ferramentas de teste do Stripe.
🔧 Cartões de Teste
// Cartões para testes
// ✅ Sucesso
4242 4242 4242 4242
// ❌ Falha genérica
4000 0000 0000 0002
// 🔐 Requer 3D Secure
4000 0025 0000 3155
// 💳 Cartão expirado
4000 0000 0000 0069
// 🚫 Fundos insuficientes
4000 0000 0000 9995📋 Checklist de Testes
- • ✅ Checkout com cartão válido
- • ❌ Checkout com cartão inválido
- • 🔄 Webhook recebido e processado
- • 👤 Customer Portal funcionando
- • 📧 Emails de confirmação enviados
- • 🗄️ Dados salvos no banco
- • 🔐 Permissões atualizadas
🚀 Próximos Passos
Recursos Avançados
- • Stripe Connect - Marketplace com múltiplos vendedores
- • Recurring Billing - Assinaturas com múltiplos planos
- • Payment Links - Links de pagamento sem código
- • Terminal API - Pagamentos presenciais
Integrações Complementares
- • Stripe Radar - Machine learning anti-fraude
- • Stripe Tax - Cálculo automático de impostos
- • Stripe Issuing - Cartões corporativos
- • Stripe Treasury - Contas bancárias programáveis
🎯 Resumo da Aula
Você aprendeu a implementar um sistema completo de pagamentos com Stripe: tela de pricing profissional, checkout seguro, webhooks para sincronização, customer portal para autoatendimento e sistema de logs para monitoramento. Esta base sólida permite escalar para milhões de transações com segurança.