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.
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
- Acesse stripe.com e crie conta
- Ative o modo de testes (Test Mode)
- Dashboard → Developers → API Keys
- Copie Publishable Key (pk_test_...)
- Copie Secret Key (sk_test_...)
- Developers → Webhooks → Add endpoint
- URL:
https://seusite.com/api/webhooks/stripe - 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
code Backend PHP - Criar Payment Intent
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
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 (
);
};
// 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 (
Pagamento Aprovado!
Seu acesso ao curso foi liberado.
Redirecionando...
shield Tratamento de Erros Comuns
Erros do Cartão
card_declined- Cartão recusado pelo bancoexpired_card- Cartão vencidoincorrect_cvc- CVV incorretoinsufficient_funds- Saldo insuficienteprocessing_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