Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Stripe Checkout Integration: Accept One-Time Payments Step by Step

๐Ÿ› ๏ธ Laravel Tutorials4 min read

Laravel 13 Stripe Checkout Integration: Accept One-Time Payments Step by Step

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
Laravel 13 Stripe Checkout Integration: Accept One-Time Payments Step by Step

The simplest and safest way to take card payments in Laravel 13 is Stripe Checkout: your server creates a Checkout Session with the stripe-php library, redirects the customer to Stripe's hosted payment page, and Stripe sends the customer back to your site. You then confirm the payment with a webhook. Card numbers never touch your server. This guide builds a one-time payment in test mode.

Targets: Laravel 13 (PHP 8.3 to 8.5) and the stripe/stripe-php library. Use test keys until everything works.

Step 1: Install the Stripe library

Code
composer require stripe/stripe-php

Step 2: Add your test API keys

In the Stripe Dashboard, switch to test mode and copy the secret key. Put it in .env:

Code
STRIPE_SECRET=sk_test_your_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
JavaScript
'stripe' => [
    'secret' => env('STRIPE_SECRET'),
    'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],

Add the array to config/services.php. Never commit secret keys, and never expose the secret key in JavaScript.

Step 3: Create the routes

CSS
use App\Http\Controllers\CheckoutController;
use Illuminate\Support\Facades\Route;

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');
Route::post('/stripe/webhook', [CheckoutController::class, 'webhook']);

Step 4: Create the Checkout Session

JavaScript
<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Stripe\Exception\SignatureVerificationException;
use Stripe\StripeClient;
use Stripe\Webhook;
use UnexpectedValueException;

class CheckoutController extends Controller
{
    public function create(): RedirectResponse
    {
        $stripe = new StripeClient(config('services.stripe.secret'));

        $session = $stripe->checkout->sessions->create([
            'mode' => 'payment',
            'line_items' => [[
                'price_data' => [
                    'currency' => 'usd',
                    'product_data' => ['name' => 'Laravel Course'],
                    'unit_amount' => 4900,
                ],
                'quantity' => 1,
            ]],
            'success_url' => route('checkout.success').'?session_id={CHECKOUT_SESSION_ID}',
            'cancel_url' => route('checkout.cancel'),
        ]);

        return redirect()->away($session->url);
    }

    public function success(Request $request)
    {
        return view('checkout.success');
    }

    public function cancel()
    {
        return view('checkout.cancel');
    }

    public function webhook(Request $request): Response
    {
        try {
            $event = Webhook::constructEvent(
                $request->getContent(),
                $request->header('Stripe-Signature', ''),
                config('services.stripe.webhook_secret')
            );
        } catch (UnexpectedValueException|SignatureVerificationException $e) {
            return response('Invalid webhook.', 400);
        }

        if ($event->type === 'checkout.session.completed') {
            $session = $event->data->object;

            // Mark the order as paid. Look it up with $session->id or your own metadata.
        }

        return response('OK', 200);
    }
}

unit_amount is in the smallest currency unit, so 4900 means 49.00 US dollars. Do not take the price from the request; read it from your own database so users cannot change it.

Create resources/views/checkout/success.blade.php and cancel.blade.php with simple messages. The success page only tells the user "thank you"; it does not prove payment, because anyone can open that URL.

Step 5: Add the payment button

Shell
<form method="POST" action="{{ route('checkout.create') }}">
    @csrf
    <button type="submit">Buy now - $49</button>
</form>

Step 6: Exclude the webhook from CSRF protection

Stripe cannot send a CSRF token, so exclude the webhook URL in bootstrap/app.php:

Shell
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'stripe/webhook',
    ]);
})

The signature check in the controller replaces CSRF for this route.

Step 7: Test the webhook locally

Install the Stripe CLI and forward events to your local server:

Code
stripe listen --forward-to http://localhost:8000/stripe/webhook

The CLI prints a signing secret that starts with whsec_. Put it into STRIPE_WEBHOOK_SECRET and run php artisan config:clear. In the Dashboard, you create the real endpoint later for production, with its own secret.

How do I test a payment?

  1. Click the Buy button. You should land on a Stripe-hosted page.
  2. Pay with the test card number 4242 4242 4242 4242, any future expiry date and any CVC.
  3. Check that you return to the success page and that the Stripe CLI shows a checkout.session.completed event with a 200 response.

Common mistakes

  • Trusting the success page: confirm payment through the webhook, not the redirect.
  • Reading the price from the form: always take it from your own data.
  • CSRF blocks the webhook: exclude the route as shown.
  • Parsing the JSON before verifying: verify the signature using the raw body from getContent().
  • Mixing test and live keys: the webhook secret must belong to the same mode and endpoint as the key.
  • Not handling duplicate events: Stripe can resend events, so make your "mark as paid" step safe to repeat.

FAQ

Should I use Laravel Cashier instead?

Cashier is a good choice for subscriptions and billing portals. For a simple one-time payment, the stripe-php flow above is small and clear.

Which payment methods are available?

Checkout shows the payment methods enabled in your Stripe settings for the customer's region. You do not hard-code them.

Do I need HTTPS?

Your live site must use HTTPS. Stripe's hosted page is already secure, and local test mode works over http://localhost.

References

Article Index & Topic Tags
LaravelStripePaymentsCheckout

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.