Integrare Stripe in un’applicazione Laravel significa poter gestire pagamenti una tantum, abbonamenti ricorrenti, rimborsi e webhooks in modo affidabile. In questa guida vediamo un percorso completo: dalla configurazione dell’account Stripe fino alla messa in produzione, con esempi sia usando Laravel Cashier (consigliato per abbonamenti) sia usando direttamente le API Stripe (consigliato per flussi personalizzati).
Prerequisiti e concetti fondamentali
- Account Stripe con chiavi API (Publishable e Secret) in modalità test e live.
- Laravel con PHP e Composer.
- Webhook endpoint raggiungibile pubblicamente in produzione (in sviluppo si può usare Stripe CLI).
Stripe espone diversi oggetti importanti:
- Payment Intent: rappresenta l’intento di pagamento; gestisce autenticazione 3D Secure e stati del pagamento.
- Customer: cliente Stripe collegato all’utente applicativo.
- Product / Price: definiscono cosa vendi e a quale prezzo (necessari per abbonamenti e Checkout).
- Checkout Session: pagina di pagamento ospitata da Stripe (spesso la via più semplice e sicura).
Installazione e configurazione di base
Inserisci le chiavi in .env. Mantieni separate test e live e non committare mai le chiavi segrete.
STRIPE_KEY=pk_test_xxxxxxxxxxxxxxxxx
STRIPE_SECRET=sk_test_xxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxx
In Laravel puoi centralizzare la configurazione in config/services.php:
<?php
return [
// ...
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook' => [
'secret' => env('STRIPE_WEBHOOK_SECRET'),
],
],
];
Approccio 1: Stripe Checkout per pagamenti una tantum
Stripe Checkout è una pagina di pagamento già pronta, localizzata, ottimizzata e aggiornata da Stripe. Riduce molto la complessità (IVA, SCA, metodi di pagamento) e limita i rischi di implementazione.
Installazione della libreria Stripe
composer require stripe/stripe-php
Crea un servizio dedicato per incapsulare l’uso di Stripe (buona pratica per testabilità e manutenzione).
<?php
namespace App\Services;
use Stripe\StripeClient;
class StripeService
{
public function client(): StripeClient
{
return new StripeClient(config('services.stripe.secret'));
}
}
Creare una Checkout Session
Esempio: acquisto singolo (importo fisso). Per importi dinamici usa price_data. In alternativa, per prodotti reali gestiti in Stripe, puoi usare direttamente un price (Price ID).
<?php
namespace App\Http\Controllers;
use App\Services\StripeService;
use Illuminate\Http\Request;
class CheckoutController extends Controller
{
public function create(Request $request, StripeService $stripe)
{
$user = $request->user();
$session = $stripe->client()->checkout->sessions->create([
'mode' => 'payment',
'customer_email' => $user->email,
'line_items' => [[
'quantity' => 1,
'price_data' => [
'currency' => 'eur',
'unit_amount' => 1990, // €19.90 in centesimi
'product_data' => [
'name' => 'Licenza Pro',
],
],
]],
'success_url' => route('checkout.success') . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout.cancel'),
'metadata' => [
'user_id' => (string) $user->id,
'order_ref' => (string) now()->timestamp,
],
]);
return redirect()->away($session->url);
}
public function success(Request $request, StripeService $stripe)
{
$sessionId = $request->query('session_id');
if (!$sessionId) {
abort(400, 'Missing session_id');
}
$session = $stripe->client()->checkout->sessions->retrieve($sessionId, [
'expand' => ['payment_intent'],
]);
// Non fidarti solo della redirect: valida anche via webhook.
return view('checkout.success', [
'session' => $session,
]);
}
public function cancel()
{
return view('checkout.cancel');
}
}
Rotte consigliate:
use App\Http\Controllers\CheckoutController;
Route::middleware(['auth'])->group(function () {
Route::post('/checkout', [CheckoutController::class, 'create'])->name('checkout.create');
Route::get('/checkout/success', [CheckoutController::class, 'success'])->name('checkout.success');
Route::get('/checkout/cancel', [CheckoutController::class, 'cancel'])->name('checkout.cancel');
});
Confermare l’ordine: perché servono i webhooks
La pagina di successo indica che l’utente è tornato sul sito, ma non è una prova definitiva di pagamento: l’utente può chiudere la finestra o forzare URL. La conferma “vera” va fatta tramite webhook, ascoltando eventi come:
checkout.session.completedpayment_intent.succeededcharge.refunded(se gestisci rimborsi)
Approccio 2: Payment Intents + Stripe Elements (flusso custom)
Se vuoi un’esperienza interamente nel tuo frontend (senza Checkout), usi Payment Intents e Stripe Elements. In questo caso il backend crea il Payment Intent e restituisce il client_secret, mentre il frontend completa la conferma con Stripe.js.
Creazione del Payment Intent lato server
<?php
namespace App\Http\Controllers;
use App\Services\StripeService;
use Illuminate\Http\Request;
class PaymentIntentController extends Controller
{
public function create(Request $request, StripeService $stripe)
{
$request->validate([
'amount' => ['required', 'integer', 'min:50'],
'currency' => ['nullable', 'string'],
]);
$user = $request->user();
$intent = $stripe->client()->paymentIntents->create([
'amount' => (int) $request->amount,
'currency' => $request->input('currency', 'eur'),
'automatic_payment_methods' => ['enabled' => true],
'metadata' => [
'user_id' => (string) $user->id,
],
]);
return response()->json([
'clientSecret' => $intent->client_secret,
]);
}
}
Rotta API tipica:
use App\Http\Controllers\PaymentIntentController;
Route::middleware(['auth:sanctum'])->post('/stripe/payment-intents', [PaymentIntentController::class, 'create']);
Frontend: conferma del pagamento
Di seguito un esempio essenziale con Stripe.js (da integrare nel tuo stack JS). Nota che questo snippet assume di aver già inizializzato Elements e di avere un endpoint che ritorna clientSecret.
import { loadStripe } from "@stripe/stripe-js";
const stripe = await loadStripe(import.meta.env.VITE_STRIPE_KEY);
// 1) Richiedi il clientSecret al backend
const res = await fetch("/api/stripe/payment-intents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 1990, currency: "eur" }),
});
const { clientSecret } = await res.json();
// 2) Conferma il pagamento con Elements
const { error } = await stripe.confirmPayment({
clientSecret,
confirmParams: {
return_url: `${window.location.origin}/pagamento/esito`,
},
});
if (error) {
console.error(error.message);
}
Anche qui: la conferma definitiva va sempre via webhook.
Abbonamenti con Laravel Cashier
Se vendi abbonamenti, Laravel Cashier (Stripe) fornisce un layer molto comodo per gestire Customer, pagamenti ricorrenti, periodi di prova e cancellazioni. Cashier dà il meglio quando modelli i piani come prodotti e prezzi su Stripe.
Installazione e migrazioni
composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
Aggiungi il trait Billable al tuo modello User:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
// ...
}
Creare una sessione Checkout per un abbonamento
Il flusso consigliato è Checkout anche per gli abbonamenti, così Stripe gestisce SCA e metodi di pagamento. Devi avere un Price ID (es. price_...) configurato su Stripe.
public function subscribe(Request $request)
{
$user = $request->user();
return $user
->newSubscription('default', 'price_1234567890')
->checkout([
'success_url' => route('billing.success') . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('billing.cancel'),
]);
}
Una volta completato il pagamento, Cashier manterrà lo stato dell’abbonamento sincronizzato se riceve i webhooks corretti.
Gestione dello stato dell’abbonamento
// Attivo?
$user->subscribed('default');
// In prova?
$user->subscription('default')?->onTrial();
// Annullare a fine periodo
$user->subscription('default')->cancel();
// Riprendere se in grace period
$user->subscription('default')->resume();
Webhooks: implementazione robusta in Laravel
I webhooks sono il punto chiave per rendere “veri” i pagamenti e reagire a eventi asincroni: pagamenti completati, fallimenti, rinnovi, chargeback, rimborsi. Implementali con verifica della firma.
Endpoint webhook con verifica della firma
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Stripe\Webhook;
class StripeWebhookController extends Controller
{
public function handle(Request $request)
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
$secret = config('services.stripe.webhook.secret');
try {
$event = Webhook::constructEvent($payload, $sigHeader, $secret);
} catch (\UnexpectedValueException $e) {
return response('Invalid payload', 400);
} catch (\Stripe\Exception\SignatureVerificationException $e) {
return response('Invalid signature', 400);
}
// Dispatch in coda per affidabilità e tempi rapidi
match ($event->type) {
'checkout.session.completed' => $this->onCheckoutCompleted($event->data->object),
'payment_intent.succeeded' => $this->onPaymentIntentSucceeded($event->data->object),
'invoice.payment_failed' => $this->onInvoicePaymentFailed($event->data->object),
default => null,
};
return response('ok', 200);
}
protected function onCheckoutCompleted($session): void
{
// Esempio: crea/chiudi ordine, abilita licenza, invia email, ecc.
// Usa $session->metadata per agganciare l’utente/ordine.
}
protected function onPaymentIntentSucceeded($intent): void
{
// Esempio: gestisci pagamenti custom basati su Payment Intents.
}
protected function onInvoicePaymentFailed($invoice): void
{
// Esempio: avvisa l’utente che il rinnovo abbonamento è fallito.
}
}
Rotta webhook (senza auth, ma con firma):
use App\Http\Controllers\StripeWebhookController;
Route::post('/stripe/webhook', [StripeWebhookController::class, 'handle'])
->name('stripe.webhook');
Idempotenza: evitare elaborazioni duplicate
Stripe può ritentare l’invio degli eventi. È quindi essenziale rendere l’elaborazione idempotente: salva l’ID dell’evento (es. evt_...) in tabella e ignora i duplicati.
<?php
// Migrazione esemplificativa:
Schema::create('stripe_events', function ($table) {
$table->id();
$table->string('event_id')->unique();
$table->string('type');
$table->timestamps();
});
// Nel controller:
if (\DB::table('stripe_events')->where('event_id', $event->id)->exists()) {
return response('duplicate', 200);
}
\DB::table('stripe_events')->insert([
'event_id' => $event->id,
'type' => $event->type,
'created_at' => now(),
'updated_at' => now(),
]);
Rimborsi e gestione dispute
Per un rimborso, in genere rimborsi la charge o il payment_intent associato. Conserva sempre i riferimenti Stripe nel tuo database (es. payment_intent_id) per poter operare senza ambiguità.
$refund = $stripe->client()->refunds->create([
'payment_intent' => $paymentIntentId,
// 'amount' => 990, // rimborso parziale in centesimi, opzionale
]);
Ascolta via webhook eventi come charge.refunded e (se rilevante per te) charge.dispute.created, per aggiornare lo stato dell’ordine e notificare il team.
Testing locale: Stripe CLI e casi reali
In sviluppo, usa Stripe CLI per inoltrare webhooks alla tua app locale. È utile per simulare eventi senza dover completare sempre un pagamento reale.
stripe login
stripe listen --forward-to http://localhost:8000/stripe/webhook
Per testare i pagamenti, Stripe fornisce carte di test e scenari (SCA, fallimenti, rimborsi). Ricorda di usare sempre chiavi test in ambiente di sviluppo.
Best practice di sicurezza e affidabilità
- Mai calcolare importi fidandoti del client: il backend deve determinare prezzo e quantità in modo autoritativo.
- Usa HTTPS in produzione; imposta correttamente i domini consentiti e le callback URL.
- Verifica sempre i webhooks tramite firma e implementa idempotenza.
- Metti in coda l’elaborazione degli eventi: evita timeout e migliora resilienza (es. queue worker).
- Log strutturati e tracciamento: salva riferimenti come
checkout_session_id,payment_intent_id,customer_id. - Gestisci gli stati: i pagamenti non sono “solo succeeded/failed”. Considera
requires_action,processing,canceled.
Checklist di messa in produzione
- Imposta chiavi live in variabili d’ambiente e ruotale se necessario.
- Configura i webhooks live su Stripe verso l’endpoint pubblico.
- Abilita i metodi di pagamento necessari (carte, wallet, bonifici, ecc.) e verifica i requisiti locali.
- Verifica fiscalità e ricevute: valuta Stripe Tax o integrazioni di fatturazione se richiesto.
- Monitora errori e ritardi della coda; prepara alert su eventi critici (pagamenti falliti, dispute).
Scelta rapida dell’approccio
- Checkout: migliore per partire velocemente e ridurre il rischio; ottimo per pagamenti una tantum e abbonamenti.
- Payment Intents + Elements: migliore se vuoi UX totalmente custom e hai bisogno di controllare ogni dettaglio.
- Cashier: ideale per abbonamenti e gestione “Laravel-friendly” di customer e subscription lifecycle.
Con questi elementi hai una base solida per implementare pagamenti con Stripe in Laravel, mantenendo sicurezza, tracciabilità e resilienza. Il passo successivo è modellare ordini e piani nel tuo dominio applicativo (tabelle orders, payments, subscriptions) e farli avanzare di stato esclusivamente in risposta ai webhooks.