Payments are one of the most typical elements of any web project, and Stripe is a very easy payment provider to install in Laravel projects. In this article, we will add a payment form to the page.
As an example, we’ll take a Product Show page from our QuickAdminPanel product management module, but you can follow the same instructions and add the Stripe form to ANY Laravel project page.
The plan will include 8 steps:
- Install Laravel Cashier
- Perform cashier migrations
- Stripe credentials in .env
- User model must be billable
- Controller: Form Payment Intent
- Page Blade: form, styles and scripts
- Controller: Post-payment processing
- After a successful purchase: send the product
Let’s get started!
1. Install Laravel Cashier
Run this command:
composer require laravel/cashier
Notice: Currently, the latest version of Cashier is v12. If you are reading this article after the new version has arrived, please read its upgrade guide. But personally, I doubt the fundamentals will change.
2. Run Cashier Migrations
The Cashier package registers its own database migration directory, so don’t forget to migrate your database after installing the package:
php artisan migrate
These migrations are not in progress database/migrations folder, they are inside /supplier. Here is the content.
1. Four new columns for users painting:
Schema::table('users', function (Blueprint $table) {
$table->string('stripe_id')->nullable()->index();
$table->string('card_brand')->nullable();
$table->string('card_last_four', 4)->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
2. New table subscriptions:
Schema::create('subscriptions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->string('name');
$table->string('stripe_id');
$table->string('stripe_status');
$table->string('stripe_plan')->nullable();
$table->integer('quantity')->nullable();
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'stripe_status']);
});
3. New board subscription_items:
Schema::create('subscription_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('subscription_id');
$table->string('stripe_id')->index();
$table->string('stripe_plan');
$table->integer('quantity');
$table->timestamps();
$table->unique(['subscription_id', 'stripe_plan']);
});
3. Stripe credentials in .env
You need to add two Stripe credentials in your .env deposit:
STRIPE_KEY=pk_test_xxxxxxxxx STRIPE_SECRET=sk_test_xxxxxxxxx
Where can we find these “keys” and “secrets”? In your Stripe dashboard:
Keep in mind that there are two “modes” of Stripe keys: essay And live keys. When you are on your local or test servers, remember to use the TEST keys, you can view them by toggling “Show Test Data” in the left menu:

Another way to know if you are using testing/live keys: testing keys start with sk_test_ And pk_test_and dynamic keys start with sk_live_ And pk_live_. Additionally, dynamic keys will not work without the SSL certificate enabled.
Notice: if you are working in a team, when you add new variables, it is a very good practice to also add them with empty values in .env.example. Your teammates will then know which variables are needed on their server. Learn more here.
4. The user model must be billable
Simple step: in your user model, add Billable Cashier line:
app/Models/User.php:
// ...
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use HasFactory, Billable;
5. Controller: Intent to pay form
To activate the Stripe payment form, we need to create an item called “payment intent” and pass it to the Blade.
In this case we will add it to Product Controller method to show():
class ProductController extends Controller
{
// ...
public function show(Product $product)
{
$intent = auth()->user()->createSetupIntent();
return view('frontend.coupons.show', compact('product', 'intent'));
}
Method createSetupIntent() comes from Billable trait that we added just above in User model.
6. Page Blade: form, styles and scripts
This is the form we will add from Stripe, with the cardholder name, card number, expiration month/year, CVV code, and zip code.

Fortunately, the Stripe documentation tells us exactly what HTML/JavaScript/CSS code needs to be added.
So, in our show.blade.phpwe add this:
<form method="POST" action="{{ route('products.purchase', $product->id) }}" class="card-form mt-3 mb-3">
@csrf
<input type="hidden" name="payment_method" class="payment-method">
<input class="StripeElement mb-3" name="card_holder_name" placeholder="Card holder name" required>
<div class="col-lg-4 col-md-6">
<div id="card-element"></div>
</div>
<div id="card-errors" role="alert"></div>
<div class="form-group mt-3">
<button type="submit" class="btn btn-primary pay">
Purchase
</button>
</div>
</form>
All input variables are exactly as Stripe suggests, the only item you will need to change is the itinerarywhere the form would be displayed, so this:
route('products.purchase', $product->id)
We will create this route and Controller method in the next step.
Meanwhile, we also need to include Stripe features. Styles And JavaScript.
Let’s imagine that in your main Blade file, you have @yield sections for styles and scripts, like this:
<!DOCTYPE html>
<html>
<head>
...
@yield('styles')
</head>
<body>
...
@yield('scripts')
</body>
</html>
Then, in our show.blade.phpwe can fill in these sections, with Stripe code:
@section('styles')
<style>
.StripeElement {
box-sizing: border-box;
height: 40px;
padding: 10px 12px;
border: 1px solid transparent;
border-radius: 4px;
background-color: white;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
</style>
@endsection
@section('scripts')
<script src="
<script>
let stripe = Stripe("{{ env('STRIPE_KEY') }}")
let elements = stripe.elements()
let style = {
base: {
color: '#32325d',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4'
}
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a'
}
}
let card = elements.create('card', {style: style})
card.mount('#card-element')
let paymentMethod = null
$('.card-form').on('submit', function (e) {
$('button.pay').attr('disabled', true)
if (paymentMethod) {
return true
}
stripe.confirmCardSetup(
"{{ $intent->client_secret }}",
{
payment_method: {
card: card,
billing_details: {name: $('.card_holder_name').val()}
}
}
).then(function (result) {
if (result.error) {
$('#card-errors').text(result.error.message)
$('button.pay').removeAttr('disabled')
} else {
paymentMethod = result.setupIntent.payment_method
$('.payment-method').val(paymentMethod)
$('.card-form').submit()
}
})
return false
})
</script>
Inside these sections, we add two back-end variables:
env('STRIPE_KEY')
And
$intent->client_secret
So make sure you have added them in the previous steps.
7. Controller: Processing after payment
Remember the route we called in the previous step? It’s time to create it.
In routes/web.phpadd this:
Route::post('products/{id}/purchase', 'ProductController@purchase')->name('products.purchase');
And then let’s create a method in Product Controller:
public function purchase(Request $request, Product $product)
{
$user = $request->user();
$paymentMethod = $request->input('payment_method');
try {
$user->createOrGetStripeCustomer();
$user->updateDefaultPaymentMethod($paymentMethod);
$user->charge($product->price * 100, $paymentMethod);
} catch (\Exception $exception) {
return back()->with('error', $exception->getMessage());
}
return back()->with('message', 'Product purchased successfully!');
}
So what’s going on here?
1. We get payment method from the form (Stripe manages it in the background for us)
2. Next we call the cashier methods to get/create the customer, set their payment method and charge them.
3. Finally, we redirect with a successful result
3b. If something goes wrong, the try/catch block handles it and redirects with an error.
Notice: variable $product->price is the price of your product, and we need to multiply it by 100 because Stripe fees are charged in cents.
To show the success message or errors, in your Blade file you need to add something like this:
@if(session('message'))
<div class="alert alert-success" role="alert">{{ session('message') }}</div>
@endif
@if(session('error'))
<div class="alert alert-danger" role="alert">{{ session('error') }}</div>
@endif
8. After a successful purchase: send the product
Once the customer pays for the product, you must deliver the order. Of course it depends on what they bought and this code is very individual, but I will show you where to put it.
Actually, there are two ways. Easier but less secure, or harder and more secure.
Option 1. Run command in ProductController
You can do it directly in the same way:
public function purchase(Request $request, Product $product)
{
$user = $request->user();
$paymentMethod = $request->input('payment_method');
try {
$user->createOrGetStripeCustomer();
$user->updateDefaultPaymentMethod($paymentMethod);
$user->charge($product->price * 100, $paymentMethod);
} catch (\Exception $exception) {
return back()->with('error', $exception->getMessage());
}
// Here, complete the order, like, send a notification email
$user->notify(new OrderProcessed($product));
return back()->with('message', 'Product purchased successfully!');
}
Easy, right? The problem with this method is that it happens synchronously, meaning that $user->load() The job may not actually be complete by the time you run the command. TheoreticallyThis may result in fake order deliveries with unsuccessful charges.
Option 2. Stripe Webhooks
A more reliable method is also to capture so-called Stripe webhooks. They ensure that the prosecution was completed successfully and in the right manner. Every time something happens in Stripe, they send a POST request to your server URL that you provide in the Stripe dashboard.
You can follow many events on Stripe, and one of these events is load.successful.
For this I would recommend using a package called Laravel Stripe Webhooks. I shot a separate video about this:
So if you want to capture more events, not just load success, I advise you to use Stripe Webhooks. Keep in mind that they won’t run (easily) on your local computer, you need to set up a real domain that Stripe would call.
That’s it! I wish you to receive a lot of successful payments in your projects.