Why Choose Stripe for Laravel 11 Payment Integration?
Stripe stands out as one of the most powerful and developer-friendly payment gateways available today, making it the top choice for Laravel applications in 2026. What makes Stripe exceptional is its robust, well-documented API that supports seamless one-time payments, recurring subscriptions, global currencies, and advanced fraud prevention tools — all while maintaining top-tier security and PCI DSS compliance. Unlike traditional gateways, Stripe never lets sensitive card data touch your server thanks to client-side tokenization via Stripe.js or Elements, reducing your compliance burden significantly.
Why integrate Stripe specifically in Laravel? Laravel's elegant syntax, built-in validation, routing, and Eloquent ORM pair perfectly with Stripe's modern API structure. This combination allows developers to build fast, scalable payment systems with minimal boilerplate code. Whether you're creating an e-commerce platform, SaaS subscription service, or marketplace, Stripe + Laravel delivers reliable, secure transactions that boost user trust and conversion rates. In this comprehensive Laravel 11 Stripe tutorial, we'll walk you through exactly how to set up a secure payment flow using the official Stripe PHP SDK, handle card payments safely, store transaction details, and prepare for production best practices like Payment Intents for SCA/3DS compliance.
By the end, you'll have a fully functional, production-ready Stripe integration ready to process real payments.

Table Of Content
1 Prerequisites
- PHP ≥ 8.2
- Composer
- MySQL database
- Stripe account (test keys from dashboard → Developers → API keys)
2 Introduction
3 Create / Install a Laravel Project
3.1 Install Laravel Project
composer create-project laravel/laravel laravel-stripe-demo
Then navigate to your project:
cd laravel-stripe-demo
3.2 Configure MySql Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_stripe_demo
DB_USERNAME=root
DB_PASSWORD=
4 Install Stripe PHP Library
composer require stripe/stripe-php
5 Configure Stripe Keys
# STRIPE
STRIPE_KEY=pk_test_96xxxxxxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET=sk_test_96xxxxxxxxxxxxxxxxxxxxxxxx
Then update config/services.php:
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
6 Create A Model and Migration
php artisan make:migration create_payments_table
Add the following fields:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->string('payment_intent_id')->unique();
$table->decimal('amount', 10, 2);
$table->string('currency', 3)->default('usd');
$table->string('status');
$table->string('card_brand')->nullable();
$table->string('card_last4')->nullable();
$table->unsignedTinyInteger('card_exp_month')->nullable();
$table->unsignedSmallInteger('card_exp_year')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('payments');
}
};
Run the migration:
php artisan migrate
Create the Payment model in app/Models/Payment.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Payment extends Model
{
use HasFactory;
protected $fillable = [
'payment_intent_id',
'amount',
'currency',
'status',
'card_brand',
'card_last4',
'card_exp_month',
'card_exp_year',
];
}
7 Create Controller ( StripeController )
php artisan make:controller StripeController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Stripe\Stripe;
use Stripe\PaymentIntent;
use App\Models\Payment;
class StripeController extends Controller
{
public function index()
{
return view('stripe.checkout');
}
public function createPaymentIntent(Request $request)
{
$request->validate([
'amount' => 'required|numeric|min:1',
]);
Stripe::setApiKey(config('services.stripe.secret'));
try {
$intent = PaymentIntent::create([
'amount' => (int) round($request->amount * 100),
'currency' => 'usd',
'automatic_payment_methods' => ['enabled' => true],
'description' => 'Laravel 11 Stripe test payment',
]);
return response()->json(['clientSecret' => $intent->client_secret]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
public function paymentSuccess(Request $request)
{
$request->validate([
'payment_intent' => 'required|string',
]);
Stripe::setApiKey(config('services.stripe.secret'));
try {
$intent = PaymentIntent::retrieve($request->payment_intent);
if ($intent->status === 'succeeded') {
$charge = $intent->charges->data[0] ?? null;
$card = $charge?->payment_method_details?->card;
Payment::create([
'payment_intent_id' => $intent->id,
'amount' => $intent->amount / 100,
'currency' => $intent->currency,
'status' => $intent->status,
'card_brand' => $card?->brand,
'card_last4' => $card?->last4,
'card_exp_month' => $card?->exp_month,
'card_exp_year' => $card?->exp_year,
]);
return response()->json(['status' => 'success']);
}
return response()->json(['error' => 'Payment not succeeded'], 400);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}
?>
8 Define a Route
use App\Http\Controllers\StripeController;
Route::get('/stripe', [StripeController::class, 'index'])->name('stripe');
Route::post('/stripe/create-payment-intent', [StripeController::class, 'createPaymentIntent']);
Route::post('/stripe/success', [StripeController::class, 'paymentSuccess']);
9 Create Blade File
Create the directory if needed: mkdir -p resources/views/stripe
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laravel 11 Stripe Payment Integration</title>
<script src="https://js.stripe.com/v3/"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card shadow">
<div class="card-body p-5">
<h2 class="card-title text-center mb-4">Pay with Stripe</h2>
<div id="messages" class="alert d-none"></div>
<form id="payment-form">
@csrf
<div class="mb-3">
<label for="amount" class="form-label">Amount (USD)</label>
<input type="number" class="form-control" id="amount" step="0.01" min="1" value="10.00" required>
</div>
<div class="mb-3">
<label class="form-label">Card Details</label>
<div id="card-element" class="form-control" style="padding: 12px;"></div>
<div id="card-errors" class="text-danger small mt-1"></div>
</div>
<button type="submit" class="btn btn-primary w-100" id="submit-btn">Pay Now</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
const stripe = Stripe('{{ config("services.stripe.key") }}');
const elements = stripe.elements();
const card = elements.create('card', { style: { base: { fontSize: '16px' } } });
card.mount('#card-element');
const form = document.getElementById('payment-form');
const submitBtn = document.getElementById('submit-btn');
const messagesDiv = document.getElementById('messages');
const cardErrors = document.getElementById('card-errors');
card.on('change', (e) => {
cardErrors.textContent = e.error ? e.error.message : '';
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
submitBtn.disabled = true;
submitBtn.textContent = 'Processing...';
messagesDiv.classList.add('d-none');
cardErrors.textContent = '';
const amount = parseFloat(document.getElementById('amount').value);
try {
const intentRes = await fetch('/stripe/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('[name="_token"]').value
},
body: JSON.stringify({ amount })
});
const intentData = await intentRes.json();
if (intentData.error) throw new Error(intentData.error);
const result = await stripe.confirmCardPayment(intentData.clientSecret, {
payment_method: {
card: card,
billing_details: { name: 'Test Customer' }
}
});
if (result.error) throw new Error(result.error.message);
// Verify & store on server
const successRes = await fetch('/stripe/success', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('[name="_token"]').value
},
body: JSON.stringify({ payment_intent: result.paymentIntent.id })
});
const successData = await successRes.json();
if (successData.status === 'success') {
showMessage('✅ Payment successful! Record saved.', 'success');
form.reset();
card.clear();
} else {
throw new Error('Server verification failed');
}
} catch (err) {
showMessage('❌ ' + err.message, 'danger');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Pay Now';
}
});
function showMessage(msg, type) {
messagesDiv.textContent = msg;
messagesDiv.className = `alert alert-${type}`;
messagesDiv.classList.remove('d-none');
}
</script>
</body>
</html>
10 Folder Structure
11 Run Laravel Server to Test the App
php artisan serve
Visit: http://127.0.0.1:8000/stripe
Testing
- Use Stripe test card: 4242 4242 4242 4242, any future expiry, any CVC
- For 3DS test: 4000 0000 0000 3220
- Check DB table payments after successful payment
12 Conclusion
Reference URL
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
You need PHP 8.2 or higher, Composer, a fresh Laravel 11 installation, and a configured MySQL database in .env. A Stripe account is required to obtain publishable and secret keys.
Run the command: composer require stripe/stripe-php. This installs the official Stripe PHP SDK.
Add STRIPE_KEY=your_publishable_key and STRIPE_SECRET=your_secret_key to your .env file. Access them in code via env('STRIPE_SECRET').
The client-side uses Stripe.js to create a secure token from card details (no raw card data touches your server). The token is sent to the controller, which uses Stripe\\Charge::create() to process the payment.
Create a 'payments' migration with fields like charge_id, transaction_id, amount, card_last_four, etc. Use a Payment model with fillable attributes to save successful charge details.
Common issues: Invalid test keys, incorrect amount (must be in cents), missing token, or using live keys with test cards. Check Stripe dashboard logs and browser console for errors.
Yes, Stripe.js tokenization ensures raw card data never hits your server (PCI compliance). Use HTTPS in production and keep secret keys server-side only.
No, this focuses on one-time charges via Charge API. For subscriptions, consider Laravel Cashier or Stripe Billing with Payment Intents.
Yes, the Charge API is legacy. For better security and features (e.g., 3D Secure), migrate to Payment Intents or use Stripe Checkout sessions.
