Introdução
O processamento de pagamentos é uma infraestrutura crítica que tem de ser implementada corretamente — erros significam perda de receita ou violações de conformidade. A Stripe disponibiliza APIs robustas para aceitar pagamentos, gerir subscrições e lidar com cenários complexos de marketplace com Stripe Connect. Este guia aborda padrões práticos de integração com a Stripe para cenários comuns de pagamento.
Configuração Inicial
Configuração da Conta
- Crie uma conta Stripe em stripe.com
- Configure o perfil do negócio em Dashboard → Settings → Profile
- Defina as moedas suportadas em Dashboard → Settings → Payouts
- Ative os métodos de pagamento em Dashboard → Settings → Payment Methods
- Gere as chaves de API em Dashboard → Developers → API Keys
Integração em PHP
composer require stripe/stripe-php
// config/services.php ou .env
STRIPE_KEY=pk_test_xxx
STRIPE_SECRET=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
// Inicializar a Stripe
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
Integração em JavaScript
npm install @stripe/stripe-js
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe('pk_test_xxx');
Checkout Sessions
Server-Side (PHP)
// Criar checkout session
public function createCheckoutSession(Request $request)
{
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price_data' => [
'currency' => 'usd',
'product_data' => [
'name' => $request->product_name,
'description' => $request->description,
'images' => [$request->image_url],
],
'unit_amount' => $request->price * 100, // Montante em cêntimos
],
'quantity' => $request->quantity,
]],
'mode' => 'payment',
'success_url' => route('checkout.success') . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout.cancel'),
'customer_email' => auth()->user()->email,
'metadata' => [
'user_id' => auth()->id(),
'order_id' => $request->order_id,
],
]);
return response()->json(['id' => $session->id]);
}
Redirecionamento no Client-Side
async function handleCheckout() {
const response = await fetch('/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: 123, quantity: 1 }),
});
const { id } = await response.json();
const stripe = await stripePromise;
const { error } = await stripe.redirectToCheckout({ sessionId: id });
if (error) {
console.error(error);
}
}
Payment Intents (UI Personalizada)
Criar Payment Intent
public function createPaymentIntent(Request $request)
{
$intent = \Stripe\PaymentIntent::create([
'amount' => $request->amount * 100,
'currency' => 'usd',
'payment_method_types' => ['card'],
'metadata' => [
'order_id' => $request->order_id,
],
]);
return response()->json([
'clientSecret' => $intent->client_secret,
]);
}
Formulário de Pagamento no Client-Side
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
function PaymentForm({ amount, orderId }) {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
setProcessing(true);
// Obter o client secret a partir do servidor
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, order_id: orderId }),
});
const { clientSecret } = await response.json();
// Confirmar pagamento
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: 'Customer Name',
},
},
}
);
setProcessing(false);
if (error) {
setError(error.message);
} else if (paymentIntent.status === 'succeeded') {
// Pagamento bem-sucedido
window.location.href = '/success';
}
}
return (
<form onSubmit={handleSubmit}>
<CardElement />
{error && <div className="error">{error}</div>}
<button disabled={!stripe || processing}>
{processing ? 'Processing...' : `Pay $${amount}`}
</button>
</form>
);
}
Webhooks
Configurar Endpoint de Webhook
// routes/web.php (excluir CSRF para webhooks)
Route::post('/webhook/stripe', [WebhookController::class, 'handle'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
Processar Eventos de Webhook
// app/Http/Controllers/WebhookController.php
public function handle(Request $request)
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$sigHeader,
config('services.stripe.webhook_secret')
);
} catch (\Exception $e) {
return response('Webhook Error', 400);
}
switch ($event->type) {
case 'checkout.session.completed':
$session = $event->data->object;
$this->handleSuccessfulPayment($session);
break;
case 'payment_intent.succeeded':
$paymentIntent = $event->data->object;
$this->handlePaymentIntentSucceeded($paymentIntent);
break;
case 'payment_intent.payment_failed':
$paymentIntent = $event->data->object;
$this->handlePaymentFailed($paymentIntent);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
case 'customer.subscription.deleted':
$subscription = $event->data->object;
$this->handleSubscriptionChange($subscription);
break;
default:
Log::info('Unhandled webhook event: ' . $event->type);
}
return response('OK', 200);
}
protected function handleSuccessfulPayment($session)
{
$orderId = $session->metadata->order_id;
$order = Order::find($orderId);
$order->update([
'status' => 'paid',
'stripe_session_id' => $session->id,
'paid_at' => now(),
]);
// Enviar e-mail de confirmação
Mail::to($order->user)->send(new OrderConfirmation($order));
}
Subscrições
Criar Subscrição
public function subscribe(Request $request)
{
$user = auth()->user();
// Criar ou obter cliente
if (!$user->stripe_customer_id) {
$customer = \Stripe\Customer::create([
'email' => $user->email,
'name' => $user->name,
]);
$user->update(['stripe_customer_id' => $customer->id]);
}
// Criar subscrição
$subscription = \Stripe\Subscription::create([
'customer' => $user->stripe_customer_id,
'items' => [
['price' => $request->price_id], // price_xxx a partir do Stripe Dashboard
],
'payment_behavior' => 'default_incomplete',
'expand' => ['latest_invoice.payment_intent'],
]);
return response()->json([
'subscriptionId' => $subscription->id,
'clientSecret' => $subscription->latest_invoice->payment_intent->client_secret,
]);
}
Cancelar Subscrição
public function cancel(Request $request)
{
$subscription = \Stripe\Subscription::retrieve($request->subscription_id);
// Cancelar no fim do período (recomendado)
$subscription->cancel_at_period_end = true;
$subscription->save();
// Ou cancelar imediatamente
// $subscription->cancel();
return response()->json(['status' => 'cancelled']);
}
Stripe Connect (Marketplaces)
Onboarding de Contas Ligadas
public function createConnectAccount(Request $request)
{
$account = \Stripe\Account::create([
'type' => 'express',
'country' => $request->country,
'email' => $request->email,
'capabilities' => [
'card_payments' => ['requested' => true],
'transfers' => ['requested' => true],
],
]);
// Criar account link para onboarding
$accountLink = \Stripe\AccountLink::create([
'account' => $account->id,
'refresh_url' => route('connect.refresh'),
'return_url' => route('connect.return'),
'type' => 'account_onboarding',
]);
// Guardar o ID da conta
$user = auth()->user();
$user->update(['stripe_connect_id' => $account->id]);
return redirect($accountLink->url);
}
Processar Pagamento de Marketplace
public function processMarketplacePayment(Request $request)
{
$seller = User::find($request->seller_id);
$platformFee = $request->amount * 0.10; // Taxa de plataforma de 10%
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => $request->amount * 100,
'currency' => 'usd',
'payment_method_types' => ['card'],
'transfer_data' => [
'destination' => $seller->stripe_connect_id,
],
'application_fee_amount' => $platformFee * 100,
]);
return response()->json([
'clientSecret' => $paymentIntent->client_secret,
]);
}
Testes
Cartões de Teste
| Número do Cartão | Cenário |
|---|
| 4242424242424242 | Pagamento bem-sucedido |
| 4000000000000077 | Aprovação automática |
| 4000000000009995 | Fundos insuficientes |
| 4000000000000069 | Cartão expirado |
| 4000002500003155 | Requer autenticação |
Testes Locais de Webhooks
# Instalar a Stripe CLI
# Transferir a partir de https://stripe.com/docs/stripe-cli
# Iniciar sessão
stripe login
# Reencaminhar webhooks para o servidor local
stripe listen --forward-to localhost:8000/webhook/stripe
# Disparar eventos de teste
stripe trigger payment_intent.succeeded
Boas Práticas de Segurança
- Nunca exponha chaves secretas em código client-side
- Verifique webhooks usando segredos de assinatura
- Use HTTPS para toda a comunicação com a Stripe
- Armazene o mínimo de dados de cartão — deixe a Stripe tratar da conformidade PCI
- Valide os montantes no server-side antes de criar cobranças
- Use chaves de idempotência para pedidos de mutação
- Registe todos os eventos de pagamento para depuração e auditoria
Idempotência: O Padrão Crítico
As repetições de rede são comuns. Sem idempotência, as repetições podem cobrar em duplicado aos clientes:
// Incluir sempre uma chave de idempotência para escritas de pagamento
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => $amount * 100,
'currency' => 'usd',
'payment_method_types' => ['card'],
], [
'idempotency_key' => 'order_' . $orderId . '_' . $attemptNumber,
]);
// Armazenar a chave com o registo do pagamento
$order->update([
'idempotency_key' => $idempotencyKey,
'stripe_payment_intent_id' => $paymentIntent->id,
]);
Isto também se aplica a reembolsos e pagamentos (payouts). Se um pedido de reembolso for repetido, não pode criar múltiplos reembolsos.
Processo de Reconciliação
Mesmo com webhooks, os dados podem divergir. Construa uma reconciliação que compare os seus registos com a Stripe:
// Tarefa diária de reconciliação
class ReconcilePayments implements ShouldQueue
{
public function handle()
{
$yesterday = now()->subDay();
// Obter cobranças a partir da Stripe
$stripeCharges = \Stripe\Charge::all([
'created' => [
'gte' => $yesterday->startOfDay()->timestamp,
'lte' => $yesterday->endOfDay()->timestamp,
],
'limit' => 100,
]);
foreach ($stripeCharges->autoPagingIterator() as $charge) {
$localPayment = Payment::where('stripe_charge_id', $charge->id)->first();
if (!$localPayment) {
Log::warning('Missing local payment record', ['charge_id' => $charge->id]);
// Criar registo em falta ou alertar
} elseif ($localPayment->amount != $charge->amount) {
Log::warning('Amount mismatch', [
'charge_id' => $charge->id,
'local' => $localPayment->amount,
'stripe' => $charge->amount,
]);
}
}
}
}
Verifique:
- Total de cobranças vs. total de encomendas
- Totais de reembolsos vs. registos de reembolso
- Payouts vs. saldos da plataforma
Reembolsos e Disputas
Modele reembolsos e disputas como estados explícitos:
// Estados do modelo de reembolso
const REFUND_STATES = [
'initiated',
'submitted',
'succeeded',
'failed',
];
public function processRefund(Order $order, string $reason, float $amount = null)
{
$refund = Refund::create([
'order_id' => $order->id,
'amount' => $amount ?? $order->amount,
'reason' => $reason,
'initiated_by' => auth()->id(),
'status' => 'initiated',
]);
try {
$stripeRefund = \Stripe\Refund::create([
'charge' => $order->stripe_charge_id,
'amount' => ($amount ?? $order->amount) * 100,
], [
'idempotency_key' => 'refund_' . $refund->id,
]);
$refund->update([
'status' => 'submitted',
'stripe_refund_id' => $stripeRefund->id,
]);
} catch (\Stripe\Exception\ApiErrorException $e) {
$refund->update(['status' => 'failed', 'error' => $e->getMessage()]);
throw $e;
}
}
Monitorizar Métricas-Chave de Pagamentos
Acompanhe métricas de visibilidade operacional:
// Métricas de pagamento para dashboards de monitorização
class PaymentMetrics
{
public function getMetrics(): array
{
return [
'success_rate' => $this->calculateSuccessRate(),
'failure_rate' => $this->calculateFailureRate(),
'webhook_processing_delay' => $this->getWebhookDelay(),
'refund_volume' => $this->getRefundVolume(),
'dispute_volume' => $this->getDisputeVolume(),
'payout_failures' => $this->getPayoutFailures(),
];
}
private function calculateSuccessRate(): float
{
$total = Payment::whereDate('created_at', today())->count();
$succeeded = Payment::whereDate('created_at', today())
->where('status', 'succeeded')->count();
return $total > 0 ? round(($succeeded / $total) * 100, 2) : 0;
}
}
Uma queda súbita nas taxas de sucesso de pagamento deve alertar a equipa de imediato.
Armadilhas Comuns
- Tratar uma resposta do cliente como sucesso de pagamento sem confirmação por webhook
- Falta de idempotência na criação de pagamentos
- Registar dados sensíveis em texto simples
- Ignorar indisponibilidades do fornecedor até os clientes reportarem problemas
- Deixar calendários de payout ambíguos
Evite isto ao desenhar fluxos de pagamento como sistemas distribuídos, e não como simples chamadas request-response.
A Mentalidade de Pagamentos de um Engenheiro Sénior
Engenheiros sénior tratam pagamentos como um sistema de alto risco que exige disciplina. Concebem para segurança, auditabilidade e recuperação. Quando os pagamentos são fiáveis, os clientes confiam na plataforma e a receita mantém-se previsível.
Conclusão
A Stripe disponibiliza uma infraestrutura de pagamentos abrangente, mas uma implementação correta é crucial para a segurança e fiabilidade. Use Checkout Sessions para fluxos simples, Payment Intents para UIs personalizadas e webhooks para confirmação fiável de pagamentos. Teste sempre exaustivamente com cartões de teste antes de entrar em produção e implemente um tratamento de erros e registo (logging) adequados. Lembre-se: pagamentos são eventos financeiros — trate-os com o respeito que merecem.