credit_card 13. Integração com Stripe - Cartão de Crédito Internacional

Stripe é o gateway de pagamentos mais usado no mundo para aceitar cartões de crédito internacionalmente. Com ele, você recebe pagamentos de qualquer lugar do mundo, suporta múltiplas moedas, e tem proteção avançada contra fraudes. Ideal para vendas únicas e assinaturas recorrentes.

Diferente do PIX, pagamentos com cartão passam por várias camadas de segurança: tokenização (você nunca manipula dados reais do cartão), 3D Secure (autenticação extra pelo banco), e webhooks para confirmar pagamentos de forma assíncrona. O Stripe cuida de tudo isso por você.

Nesta seção, você vai implementar: formulário de cartão seguro usando Stripe Elements (componentes prontos com validação), criação de Payment Intents no backend, processamento de webhooks, e tratamento de erros como cartão recusado ou autenticação pendente.

info
Conceito Chave: Com Stripe, você NUNCA toca em dados sensíveis de cartão. Tudo é tokenizado no frontend antes de chegar ao seu servidor. Isso mantém você em conformidade com PCI-DSS.

timeline Fluxo de Pagamento com Stripe

┌─────────────────────────────────────────────────────────────┐
│           FLUXO DE PAGAMENTO STRIPE (CREDIT CARD)           │
└─────────────────────────────────────────────────────────────┘

1. USUÁRIO ACESSA CHECKOUT
   React → Carregar Stripe.js e Elements
   
2. CRIAR PAYMENT INTENT (Backend)
   React → POST /api/stripe/create-payment-intent
   {
     courseId: 123,
     amount: 4990, // R$ 49,90 em centavos
     currency: "brl"
   }
   
   PHP Backend → Stripe API
   POST https://api.stripe.com/v1/payment_intents
   Headers: Authorization: Bearer sk_test_...
   
   Response:
   {
     id: "pi_abc123",
     client_secret: "pi_abc123_secret_xyz",
     status: "requires_payment_method"
   }
   
3. FRONTEND EXIBE FORMULÁRIO
   React → Renderiza Stripe CardElement
   Usuário preenche: número, validade, CVV
   
4. CONFIRMAR PAGAMENTO (Frontend)
   stripe.confirmCardPayment(clientSecret, {
     payment_method: {
       card: cardElement,
       billing_details: { name, email }
     }
   })
   
   → Stripe tokeniza dados do cartão
   → Envia para servidores da Stripe
   → NÃO passa pelo seu backend
   
5. PROCESSAMENTO 3D SECURE (Se necessário)
   Stripe → Redireciona para banco emissor
   Usuário → Confirma SMS/App do banco
   Stripe → Retorna sucesso/falha
   
6. STRIPE PROCESSA PAGAMENTO
   Status: "processing" → "succeeded" ou "failed"
   
7. WEBHOOK NOTIFICA BACKEND
   Stripe → POST /api/webhooks/stripe
   {
     type: "payment_intent.succeeded",
     data: {
       object: { id: "pi_abc123", amount: 4990, ... }
     }
   }
   
8. BACKEND LIBERA ACESSO
   PHP → Valida webhook signature
   PHP → Atualiza order.status = "paid"
   PHP → Insere em user_courses
   PHP → Envia email de confirmação
   
9. FRONTEND REDIRECIONA
   React → Detecta sucesso
   React → Redireciona para /courses/123

settings Configuração Inicial - Stripe

key 1. Criar Conta Stripe

  1. Acesse stripe.com e crie conta
  2. Ative o modo de testes (Test Mode)
  3. Dashboard → Developers → API Keys
  4. Copie Publishable Key (pk_test_...)
  5. Copie Secret Key (sk_test_...)
  6. Developers → Webhooks → Add endpoint
  7. URL: https://seusite.com/api/webhooks/stripe
  8. Eventos: payment_intent.succeeded, payment_intent.payment_failed

lock 2. Variáveis de Ambiente

# Backend (.env)
STRIPE_SECRET_KEY=sk_test_XXXXXXXXXXXXX
STRIPE_WEBHOOK_SECRET=whsec_YYYYYYYYYYYY

# Frontend (.env)
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_ZZZZZZZZZZ
error
NUNCA exponha a Secret Key no frontend! Use apenas Publishable Key.

code Backend PHP - Criar Payment Intent

warning
Instalação: composer require stripe/stripe-php
// api/stripe/create-payment-intent.php
 'Não autenticado']);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Método não permitido']);
    exit;
}

$data = json_decode(file_get_contents('php://input'), true);
$userId = $_SESSION['user_id'];
$courseId = $data['courseId'] ?? null;

if (!$courseId) {
    http_response_code(400);
    echo json_encode(['error' => 'courseId é obrigatório']);
    exit;
}

try {
    $db = getDBConnection();
    
    // 1. Buscar curso
    $stmt = $db->prepare("SELECT id, title, price FROM courses WHERE id = ?");
    $stmt->execute([$courseId]);
    $course = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$course) {
        http_response_code(404);
        echo json_encode(['error' => 'Curso não encontrado']);
        exit;
    }
    
    // 2. Buscar usuário
    $stmt = $db->prepare("SELECT id, name, email FROM users WHERE id = ?");
    $stmt->execute([$userId]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
    
    // 3. Criar pedido no banco local
    $stmt = $db->prepare("
        INSERT INTO orders (user_id, course_id, amount, payment_method, status, currency)
        VALUES (?, ?, ?, 'credit_card', 'pending', 'brl')
    ");
    $stmt->execute([$userId, $courseId, $course['price']]);
    $orderId = $db->lastInsertId();
    
    // 4. Configurar Stripe
    Stripe::setApiKey($_ENV['STRIPE_SECRET_KEY']);
    
    // 5. Criar Payment Intent no Stripe
    $paymentIntent = PaymentIntent::create([
        'amount' => (int) $course['price'], // Centavos
        'currency' => 'brl',
        'description' => $course['title'],
        'metadata' => [
            'order_id' => $orderId,
            'user_id' => $userId,
            'course_id' => $courseId
        ],
        'receipt_email' => $user['email'],
        'automatic_payment_methods' => [
            'enabled' => true,
        ],
    ]);
    
    // 6. Salvar Payment Intent ID no banco
    $stmt = $db->prepare("
        UPDATE orders SET 
            stripe_payment_intent_id = ?
        WHERE id = ?
    ");
    $stmt->execute([$paymentIntent->id, $orderId]);
    
    // 7. Retornar client_secret para o frontend
    echo json_encode([
        'success' => true,
        'clientSecret' => $paymentIntent->client_secret,
        'orderId' => $orderId
    ]);
    
} catch (Exception $e) {
    error_log("Erro ao criar Payment Intent: " . $e->getMessage());
    http_response_code(500);
    echo json_encode([
        'error' => 'Erro ao processar pagamento',
        'message' => $e->getMessage()
    ]);
}

webhook Backend PHP - Webhook Handler

// api/webhooks/stripe.php
prepare("
    INSERT INTO webhooks_log (source, event_type, payload)
    VALUES ('stripe', ?, ?)
");
$stmt->execute([$event->type, $payload]);
$webhookLogId = $db->lastInsertId();

// 3. Processar evento
try {
    switch ($event->type) {
        case 'payment_intent.succeeded':
            handlePaymentIntentSucceeded($db, $event->data->object, $webhookLogId);
            break;
            
        case 'payment_intent.payment_failed':
            handlePaymentIntentFailed($db, $event->data->object, $webhookLogId);
            break;
            
        case 'charge.refunded':
            handleChargeRefunded($db, $event->data->object, $webhookLogId);
            break;
            
        default:
            error_log("Evento não tratado: {$event->type}");
    }
    
    // Marcar como processado
    $stmt = $db->prepare("
        UPDATE webhooks_log SET processed = TRUE, processed_at = NOW()
        WHERE id = ?
    ");
    $stmt->execute([$webhookLogId]);
    
    http_response_code(200);
    
} catch (Exception $e) {
    error_log("Erro ao processar webhook: " . $e->getMessage());
    
    $stmt = $db->prepare("
        UPDATE webhooks_log SET error_message = ?
        WHERE id = ?
    ");
    $stmt->execute([$e->getMessage(), $webhookLogId]);
    
    http_response_code(500);
}

/**
 * Handler: Pagamento Bem-Sucedido
 */
function handlePaymentIntentSucceeded($db, $paymentIntent, $webhookLogId) {
    $piId = $paymentIntent->id;
    
    // Buscar pedido
    $stmt = $db->prepare("SELECT * FROM orders WHERE stripe_payment_intent_id = ?");
    $stmt->execute([$piId]);
    $order = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$order) {
        throw new Exception("Pedido não encontrado: {$piId}");
    }
    
    // Evitar processamento duplicado
    if ($order['status'] === 'paid') {
        error_log("Pedido já foi pago: {$order['id']}");
        return;
    }
    
    $db->beginTransaction();
    
    try {
        // Atualizar pedido
        $stmt = $db->prepare("
            UPDATE orders SET 
                status = 'paid',
                paid_at = NOW()
            WHERE id = ?
        ");
        $stmt->execute([$order['id']]);
        
        // Liberar acesso ao curso
        $stmt = $db->prepare("
            INSERT INTO user_courses (user_id, course_id, purchased_at)
            VALUES (?, ?, NOW())
            ON DUPLICATE KEY UPDATE purchased_at = NOW()
        ");
        $stmt->execute([$order['user_id'], $order['course_id']]);
        
        // Log de auditoria
        $stmt = $db->prepare("
            INSERT INTO audit_log (user_id, action, details)
            VALUES (?, 'course_purchased_stripe', ?)
        ");
        $stmt->execute([
            $order['user_id'],
            json_encode([
                'order_id' => $order['id'],
                'course_id' => $order['course_id'],
                'amount' => $order['amount'],
                'payment_intent_id' => $piId,
                'webhook_log_id' => $webhookLogId
            ])
        ]);
        
        $db->commit();
        
        error_log("Pagamento Stripe processado: Order #{$order['id']}");
        
    } catch (Exception $e) {
        $db->rollBack();
        throw $e;
    }
}

/**
 * Handler: Pagamento Falhou
 */
function handlePaymentIntentFailed($db, $paymentIntent, $webhookLogId) {
    $piId = $paymentIntent->id;
    
    $stmt = $db->prepare("
        UPDATE orders SET status = 'failed'
        WHERE stripe_payment_intent_id = ?
    ");
    $stmt->execute([$piId]);
    
    error_log("Pagamento Stripe falhou: {$piId}");
}

/**
 * Handler: Reembolso
 */
function handleChargeRefunded($db, $charge, $webhookLogId) {
    $piId = $charge->payment_intent;
    
    $stmt = $db->prepare("SELECT * FROM orders WHERE stripe_payment_intent_id = ?");
    $stmt->execute([$piId]);
    $order = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if ($order) {
        $db->beginTransaction();
        
        try {
            $stmt = $db->prepare("UPDATE orders SET status = 'refunded' WHERE id = ?");
            $stmt->execute([$order['id']]);
            
            $stmt = $db->prepare("
                DELETE FROM user_courses
                WHERE user_id = ? AND course_id = ?
            ");
            $stmt->execute([$order['user_id'], $order['course_id']]);
            
            $db->commit();
            
            error_log("Reembolso Stripe processado: Order #{$order['id']}");
            
        } catch (Exception $e) {
            $db->rollBack();
            throw $e;
        }
    }
}

code Frontend React - Componente de Pagamento

warning
Instalação: npm install @stripe/react-stripe-js @stripe/stripe-js
// components/StripeCheckout.jsx
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import {
    Elements,
    CardElement,
    useStripe,
    useElements
} from '@stripe/react-stripe-js';
import axios from 'axios';

const API_URL = import.meta.env.VITE_API_URL;
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);

const CheckoutForm = ({ courseId, courseName, coursePrice, onSuccess }) => {
    const stripe = useStripe();
    const elements = useElements();
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');

    const handleSubmit = async (e) => {
        e.preventDefault();

        if (!stripe || !elements) {
            return;
        }

        setLoading(true);
        setError(null);

        try {
            // 1. Criar Payment Intent no backend
            const { data } = await axios.post(
                `${API_URL}/stripe/create-payment-intent`,
                { courseId },
                { withCredentials: true }
            );

            const clientSecret = data.clientSecret;

            // 2. Confirmar pagamento com dados do cartão
            const result = await stripe.confirmCardPayment(clientSecret, {
                payment_method: {
                    card: elements.getElement(CardElement),
                    billing_details: {
                        name,
                        email
                    }
                }
            });

            if (result.error) {
                // Erro (cartão recusado, etc.)
                setError(result.error.message);
                setLoading(false);
            } else {
                if (result.paymentIntent.status === 'succeeded') {
                    // Pagamento aprovado!
                    onSuccess(result.paymentIntent);
                }
            }

        } catch (err) {
            console.error('Erro:', err);
            setError('Erro ao processar pagamento. Tente novamente.');
            setLoading(false);
        }
    };

    const cardStyle = {
        style: {
            base: {
                color: '#1C1C1E',
                fontFamily: 'Inter, sans-serif',
                fontSize: '16px',
                '::placeholder': {
                    color: '#6B7280',
                },
            },
            invalid: {
                color: '#EF4444',
            },
        },
    };

    return (
        

{courseName}

{(coursePrice / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}

setName(e.target.value)} required placeholder="João Silva" />
setEmail(e.target.value)} required placeholder="joao@email.com" />

lock Pagamento seguro processado pela Stripe

{error && (
error
{error}
)}
🔒 Protegido por 3D Secure ✅ Certificação PCI-DSS
); }; // Componente Principal com Stripe Provider export const StripeCheckout = ({ courseId, courseName, coursePrice }) => { const [paymentSuccess, setPaymentSuccess] = useState(false); const handleSuccess = (paymentIntent) => { setPaymentSuccess(true); // Redirecionar após 2 segundos setTimeout(() => { window.location.href = `/courses/${courseId}`; }, 2000); }; if (paymentSuccess) { return (
check_circle

Pagamento Aprovado!

Seu acesso ao curso foi liberado.

Redirecionando...

); } return ( ); };

shield Tratamento de Erros Comuns

Erros do Cartão

  • card_declined - Cartão recusado pelo banco
  • expired_card - Cartão vencido
  • incorrect_cvc - CVV incorreto
  • insufficient_funds - Saldo insuficiente
  • processing_error - Erro temporário

Tratamento no Frontend

if (result.error) {
    const errorMessages = {
        card_declined: 'Cartão recusado. Tente outro.',
        expired_card: 'Cartão vencido.',
        incorrect_cvc: 'CVV incorreto.',
        insufficient_funds: 'Saldo insuficiente.'
    };
    
    setError(errorMessages[result.error.code] || result.error.message);
}

check_circle Comparação: PIX vs Stripe

pix PIX (Pagar.me)

  • ✅ Instantâneo (1-5 segundos)
  • ✅ Taxa baixa (1-2%)
  • ✅ Apenas Brasil
  • ✅ Sem chargebacks
  • ❌ Pagamento único (sem recorrência)
  • ❌ Não funciona fora do Brasil

credit_card Stripe (Cartão)

  • ✅ Internacional (135+ países)
  • ✅ Suporta assinaturas
  • ✅ Múltiplas moedas
  • ✅ Proteção avançada contra fraude
  • ❌ Taxa maior (3-5%)
  • ❌ Risco de chargeback
lightbulb
Recomendação: Ofereça ambos! PIX para usuários brasileiros e Stripe para pagamentos internacionais. Maximize suas vendas cobrindo todos os públicos.
rocket_launch
Próximo Passo: Agora você tem PIX e Cartão funcionando! Na próxima seção, vamos criar verificação de compra de cursos para garantir que apenas quem pagou acessa o conteúdo.