How to implement razorpay subscription in laravel?

This tutorial offers a detailed, step-by-step procedure on how to incorporate the Razorpay payment gateway into a Laravel or PHP application using the Razorpay package. If you don't have the necessary credentials for the Razorpay Payment Gateway, I recommend reading our post on obtaining Razorpay API Keys.


Please be aware that this article will be quite extensive, therefore, basic fundamental aspects such as the creation and management of the plan module will not be covered.


We'll be utilizing the Razorpay package for this process. To install the Razorpay package, please execute the provided command.

composer require razorpay/razorpay
Step 1 : Create a controller.

To create a controller please execute the provided command.

php artisan make:controller PaymentController
Step 2 : Create a payment form.

Now we will create a payment form so you can ask user for the detials and conformation you need, So first lets create a route in web.php

//PAYMENT FORM
Route::get('payment-form', [\App\Http\Controllers\PaymentController::class, 'index'])->name('payment-form');

Now create a function named index in PaymentController.php

public function index()
{
    $plans =
    [
        0 => [
            'name' => 'free',
            'amount' => 0
        ],
        1 => [
            'name' => 'daily',
            'amount' => 2
        ],
        2 => [
            'name' => 'weekly',
            'amount' => 10
        ],
        3 => [
            'name' => 'monthly',
            'amount' => 30
        ],
        4 => [
            'name' => 'yearly',
            'amount' => 300
        ],
    ];

    return view('razorpay.subscription-form',compact('plans'));
}

Now lets create a payment form,

resources/views/razorpay/subscription-form.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Razorpay Payment Gateway</title>

    <!-- BOOTSTRAP -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

    <!-- Styles -->
    <style>
        body {
            background: #f7f7f7;
        }

        .form-box {
            max-width: 500px;
            margin: auto;
            padding: 50px;
            background: #ffffff;
            border: 10px solid #f2f2f2;
            margin-top: 100px;
        }

        h1, p {
            text-align: center;
        }

        input, textarea {
            width: 100%;
        }
    </style>
</head>
<body>
<div class="form-box">
    @if (\Session::has('success'))
        <div class="alert alert-success">
            <strong>Success : </strong> {!! \Session::get('success') !!}
        </div>
        {{ \Session::forget('success') }}
    @endif
    @if (\Session::has('error'))
        <div class="alert alert-danger">
            <strong>Error : </strong> {!! \Session::get('error') !!}
        </div>
        {{ \Session::forget('error') }}
    @endif

    <h1>Pay with Razorpay</h1>
    <form action="" method="post" style="text-align: center;margin-top: 50px;">
        @csrf
        <div class="form-group">
            <label for="plan">Plan</label>
            <select class="form-control" name="plan" id="plan">
                <option value="">Select Plan</option>
                @foreach($plans as $plan)
                    <option value="{{ $plan['name'] }}" data-amount="{{ $plan['amount'] }}">{{ $plan['name'] }}</option>
                @endforeach
            </select>
        </div>
        <div class="form-group">
            <label for="amount">Amount</label>
            <input class="form-control" id="amount" type="text" name="amount" readonly>
        </div>
        <button type="button" class="btn btn-primary" id="buy-btn">Subscribe</button>
    </form>
</div>
<!--JS starts-->
 <script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<script>
    $(document).on('change','#plan',function()
    {
        var element = $(this).find('option:selected');
        var amount = element.data('amount');
        $('#amount').val(amount);
    });
</script>
<!--JS ends-->
</body>
</html>
Step 3 : Create Plan & Subscription with Razorpay

First lets create a route in web.php

Route::post('generate-razorpay-payment', [\App\Http\Controllers\PlanController::class, 'create'])->name('generate-razorpay-payment');

Now create a function named create in PaymentController.php

use Illuminate\Http\Request;
use Razorpay\Api\Api;


public function create(Request $request)
{
    // Define your Razorpay API credentials
    $public_key = 'rzp_********************';
    $secret_key = 'i3jR********************';

    // Create a new instance of the Razorpay API
    $api = new Api($public_key,$secret_key);

    // Define the interval for each plan type
    // daily = 7
    // weekly = 5
    // monthly = 1
    // yearly = 1

    // Determine the interval based on the selected plan
    if($request->plan == 'daily')
    {
        $interval = 7;
    }
    elseif($request->plan == 'weekly')
    {
        $interval = 5;
    }
    else
    {
        // For monthly & yearly plans
        $interval = 1;
    }

    // Define the plan name, period, and amount
    $planName = 'CodeOnString Test Plan';
    $period = $request->plan;
    $amount = $request->amount * 100;

    // Prepare the data for the plan
    $planArrData = array(
        'period' => $period,
        'interval' => $interval,
        'item' => array(
            'name' => $planName,
            'description' => ''",
            'amount' => $amount,
            'currency' => 'INR'
        )
    );

    // Create the plan using the Razorpay API
    $createRzpPlan = $api->plan->create($planArrData);

    // Get the ID of the created plan
    $planID = $createRzpPlan->id;

    // Define the subscription duration in months
    $subs_time = 12;

    // Prepare the data for the subscription
    $subscriptionArrData = array(
        'plan_id' => $planID,
        'total_count' => $subs_time,
    );

    // Create the subscription using the Razorpay API
    $createRzpsubscription = $api->subscription->create($subscriptionArrData);

    // Prepare the options for the checkout
    $options =
        [
            "key" => $public_key,
            "name" => 'CodeOnString', // Your business name
            "description" => "Plan Purchase",
            "image"=> 'https://codeonstring.com/assets/images/logos/favicon.png',
            "subscription_id" => $createRzpsubscription->id,
            "theme"=> [
                "color"=> "#3399cc"
            ]
        ];

    // Return the checkout data and status
    return response()->json(['checkoutData' => $options, 'status' => true]);
}
Step 4 : Javascript

Now lets add javascript to manage free plan purchase and razorpay subscription purchase, Open this file and add javascript on bottom


resources/views/razorpay/subscription-form.blade.php

$(document).on('click','#buy-btn',function()
{
    var $this = $(this);
    $this.html('Processing...');

    var subscriptionForm = $(this).parents('form:first');
    var formData = subscriptionForm.serializeArray();

    var amount = $('#amount').val();
    if(amount > 0)
    {
        purchasePlan(formData,subscriptionForm);
    }
    else
    {
        if (confirm("Are you sure you want to buy free plan?") == false)
        {
            $this.html('Subscribe');
            return false;
        }
        subscriptionForm.attr('action','{{ route('purchase-free-plan') }}');
        subscriptionForm.submit();
    }
});
function purchasePlan(formData,subscriptionForm)
{
    var URL = "{{ route('generate-razorpay-payment') }}";
    $.ajax({
        url: URL,
        dataType: "json",
        data: formData,
        type: "POST",
        cache: false,
        success: function (response) {
            if (response.status === true)
            {
                proceedPayment(response.checkoutData, subscriptionForm);
            }
            else
            {
                //show error message
                console.log(response);
            }
        },
        error: function (response)
        {
            //show error message
            $('#buy-btn').html('Subscribe')
            console.log(response.responseJSON.message);
        }
    });
}
var proceedPayment = function(dta,subscriptionForm)
{
    var options = dta;
    options.handler = function(response)
    {
        subscriptionForm.attr('action','{{ route('razorpay-callback') }}');

        $('.razorpay-field').remove();
        subscriptionForm.append('<input type="hidden" class="razorpay-field" name="razorpay_payment_id" value="'+response.razorpay_payment_id+'">');
        subscriptionForm.append('<input type="hidden" class="razorpay-field" name="razorpay_signature" value="'+response.razorpay_signature+'">');
        subscriptionForm.append('<input type="hidden" class="razorpay-field" name="razorpay_subscription_id" value="'+response.razorpay_subscription_id+'">');
        subscriptionForm.submit();
    }
    Razorpay.open(options);
}
Step 5 : Free Plan Purchase

Now we will make callback and free plan purchase functions in PaymentController, So first lets add some routes in web.php

// FREE PLAN PURCHASE
Route::post('purchase-free-plan', [\App\Http\Controllers\PaymentController::class, 'freePlan'])->name('purchase-free-plan');
// RAZORPAY PAYMENT CALLBACK
Route::post('razorpay-callback', [\App\Http\Controllers\PaymentController::class, 'callback'])->name('razorpay-callback');

Essentials fields for database to manage subscription :

1. Plan ID (optional)

2. Plan Details (optional)

3. Valid Till (timestamp | required to track payment time and status)

4. Amount (optional)

5. Plan Type : Free / Paid (required to check if payment is free or paid)

6. Subscription ID ('Free' for free plan but subscription_id when purchasing with razorpay)

public function purchaseFreePlan(Request $request)
{
    // save details here
    dd($request->all());
}
Step 5 : Callback

Now we need to create callback function in PaymentController.php to handle payment status and storing it to database.

public function callback(Request $request)
{
    // Define your Razorpay API credentials
    $public_key = 'rzp_********************';
    $secret_key = 'i3jR********************';

    // Create a new instance of the Razorpay API
    $api = new Api($public_key, $secret_key);

    // Get the payment ID from the request
    $payment_id = $request->razorpay_payment_id;

    try
    {
        // Fetch payment details from Razorpay using the payment ID
        $payment = $api->payment->fetch($payment_id);

        // Check if the payment status is 'authorized' or 'captured'
        if ($payment->status == 'authorized' || $payment->status == 'captured')
        {
            // NOTE: Make sure to save the subscription ID; it will be used to verify the next term payment

            // Get the subscription ID from the request
            $subscription_id = $request->razorpay_subscription_id;

            // Signature (Not required or important for subscription management; optional)
            $razorpay_signature = $request->razorpay_signature;

            // To fetch payment details in the future, save the payment ID (optional for subscription management)

            // Redirect back with a success message
            return redirect()->back()->with('success', __('Plan Purchased Successfully'));
        }
        else
        {
            // Redirect back with an error message if payment status is not authorized or captured
            return redirect()->back()->with('error', __('Payment failed'));
        }
    }
    catch (\Exception $e)
    {
        // Handle exceptions (e.g., invalid payment ID, API errors)
        return redirect()->back()->with('error', $e->getMessage());
    }
}

Explanation:

1. The callback function handles the response from the Razorpay payment gateway after a transaction.

2. It initializes the Razorpay API with the provided public and secret keys.

3. Retrieves the payment ID from the request.

4. Fetches payment details from Razorpay using the payment ID.

5. Checks if the payment status is authorized or captured.

6. If successful, saves the subscription ID (for future verification) and optionally the payment ID.

7. Redirects back with a success message.

8. If payment status is not authorized or captured, redirects back with an error message.

9. Handles exceptions (e.g., invalid payment ID, API errors) and provides an error message.

Step 6 : Usage

When you click the "Subscribe" button, a Razorpay-generated popup checkout will appear. It will look something like this :


Both merchant and customer will recive a mail which will look like this : 



Step 7 : Managing Periodic Subscription Payment.

The plan purchase process has been successfully completed. Now, let's proceed to manage periodic subscription payments using middleware. To get started, please execute the provided command.

php artisan make:middleware VerifySubscriptionPayment

Now a middleware named VerifySubscriptionPayment.php will be generated in : app/Http/Middleware/VerifySubscriptionPayment.php

<?php

namespace App\Http\Middleware;

use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
use Razorpay\Api\Api;
use Symfony\Component\HttpFoundation\Response;

class VerifySubscriptionPayment
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        // auth user data
        $user = auth()->user();

        // call payment details based on your database structure
        $valid_till = Carbon::parse('2024-06-19 00:00:00');
        $subscription_id = 'sub_**************'; // it should be 'Free' if purchase was free

        if (!empty($valid_till))
        {
            if ($valid_till->isPast())
            {
                // plan expired, need to check razorpay subscription
                if(!empty($subscription_id) && $subscription_id != 'Free')
                {
                    // Define your Razorpay API credentials
                    $public_key = 'rzp_********************';
        			$secret_key = 'i3jR********************';

                    // Create a new instance of the Razorpay API
                    $api = new Api($public_key, $secret_key);

                    // Fetch subscription details from Razorpay using the provided subscription ID
                    $subscription = $api->subscription->fetch($subscription_id);

                    // Get the timestamp when the next payment is due
                    $charge_at = $subscription->charge_at;

                    // Convert the timestamp to a human-readable date and time format
                    $nextDate = date('Y-m-d H:i:s', $charge_at);
                    $nextPaymentDate = Carbon::parse($nextDate);

                    // Get the status of the subscription (e.g., active, canceled, authenticated, completed etc.)
                    $status = $subscription->status;
                }
                else
                {
                    // purchased free plan before i.e. free plan expired
                    $status = 'completed';
                }

                if($status == 'active')
                {
                    // subscription payment is paid by user i.e. subscription active, so update valid till timestamp in database

                    // $valid_till = $nextPaymentDate;

                    return $next($request);
                }
                elseif($status == 'authenticated')
                {
                    // payment is completed but waiting for conformation, it happens rarely where you have to confirm payment manually form razorpay side, you can skip it or show 'waiting for conformation' message
                    return new \Illuminate\Http\Response(view('razorpay.middleware.plans.payment', compact('user','status')));
                }
                elseif($status == 'completed')
                {
                    // free subscription expired need to subscribe
                    $plans =
                        [
                            0 => [
                                'name' => 'daily',
                                'amount' => 2
                            ],
                            1 => [
                                'name' => 'weekly',
                                'amount' => 10
                            ],
                            2 => [
                                'name' => 'monthly',
                                'amount' => 30
                            ],
                            3 => [
                                'name' => 'yearly',
                                'amount' => 300
                            ],
                        ];

                    return new \Illuminate\Http\Response(view('razorpay.subscription-form', compact('plans')));
                }
                else
                {
                    // payment is pending, show subscription link and message
                    $paymentLink = $subscription->short_url;
                    return new \Illuminate\Http\Response(view('razorpay.middleware.plans.payment', compact('paymentLink','user','status')));
                }
            }
            else
            {
                // plan is active
                return $next($request);
            }
        }
        else
        {
            // have not purchased plan (new user, so you can show free plan)
            $plans =
                [
                    0 => [
                        'name' => 'free',
                        'amount' => 0
                    ],
                    1 => [
                        'name' => 'daily',
                        'amount' => 2
                    ],
                    2 => [
                        'name' => 'weekly',
                        'amount' => 10
                    ],
                    3 => [
                        'name' => 'monthly',
                        'amount' => 30
                    ],
                    4 => [
                        'name' => 'yearly',
                        'amount' => 300
                    ],
                ];

            return new \Illuminate\Http\Response(view('razorpay.subscription-form', compact('plans')));
        }
    }
}

Now lets create a blade file to manage payment message and payment link on frontend side


resources/views/razorpay/middleware/plans/payment.blade.php


If payment is pending we will show payment link 


If payment is completed but waiting for conformation then we need to show 'waiting for conformation' message, It happens rarely where you have to confirm payment manually form razorpay side.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Razorpay Payment Gateway</title>

    <!-- BOOTSTRAP -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

    <!-- Styles -->
    <style>
        body {
            background: #f7f7f7;
        }

        .form-box {
            max-width: 500px;
            margin: auto;
            padding: 50px;
            background: #ffffff;
            border: 10px solid #f2f2f2;
            margin-top: 100px;
        }

        h1, p {
            text-align: center;
        }

        input, textarea {
            width: 100%;
        }
    </style>
</head>
<body>
<div class="form-box">
    @if (\Session::has('success'))
        <div class="alert alert-success">
            <strong>Success : </strong> {!! \Session::get('success') !!}
        </div>
        {{ \Session::forget('success') }}
    @endif
    @if (\Session::has('error'))
        <div class="alert alert-danger">
            <strong>Error : </strong> {!! \Session::get('error') !!}
        </div>
        {{ \Session::forget('error') }}
    @endif

    @if($status == 'authenticated')
        <div style="text-align: center;">
            <h1>Payment Completed</h1>
            <p>Payment has been completed, Waiting for conformation.</p>
        </div>
    @else
        <div style="text-align: center;">
            <h1>Payment Pending</h1>
            <p>To start using our service again, Please complete the payment.</p>
            @if(isset($paymentLink))
                <a href="{{ $paymentLink }}" class="btn btn-primary" style="margin-top: 30px;">Pay Now</a>
            @endif
        </div>
    @endif
</div>
</body>
</html>
Step 9 : Usage

When user click the "Pay Now" button, They will be redirected to Razorpay-generated payment page. Which will look something like this :


Step 10 : Reminder (Optional)

We can set up an optional process to notify users about their upcoming payments. To achieve this, we'll use a cron job with task scheduling.


To create a task please execute the provided command.

php artisan make:command PaymentReminder

Now a file named PaymentReminder.php will be generated in : app/Console/Commands/PaymentReminder.php


Note  :  I am using eloquent relationship of belongsTo, To define a many-to-one relationship between two models.

<?php

namespace App\Console\Commands;

use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;

class PaymentReminder extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:payment-reminder';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = "Check if user's subscription is active or not.";

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $users = User::all();
        foreach($users as $user)
        {
            if(!empty($user->plan->valid_till))
            {
                if ($user->plan->valid_till->isPast())
                {
                    // NOTIFY THAT PAYMENT DUE HAS BEEN PASSED
                }
                else
                {
                    $now = Carbon::now();
                    $daysLeft = $user->plan->valid_till->diff($now)->days;

                    // CHANGE NUMBERS / DAYS BASED ON YOUR REQUIREMENTS
                    if ($daysLeft == 3)
                    {
                        // NOTIFY : THERE IS A PAYMENT 2 DAYS
                    }

                    if ($daysLeft == 1)
                    {
                        // NOTIFY : PAYMENT IS ON TOMORROW
                    }

                    if ($daysLeft == 0)
                    {
                        // NOTIFY : TODAY IS PAYMENT DATE
                    }
                }
            }
        }

        return $this->info("Payment reminder sent.");
    }
}

1. Open the app/Console/Kernel.php file. This file contains the schedule method, where you can define your scheduled tasks.

2. Suppose you want to run a task every day at midnight. Add the following code inside the schedule method :


To ensure your project works with the correct time, make sure to set the proper timezone. By default, the timezone is UTC. To change the timezone, open the config/app.php file.

protected function schedule(Schedule $schedule): void
{
    $schedule->command('payment-reminder')->dailyAt('00:00');
}

Prior to configuring the cron job, you can manually test your custom task using the following command :

php artisan app:payment-reminder

To configure a cron job on your server, you'll need to add the following command :

0 0 * * * cd /path/to/your/project && php artisan schedule:run >> /dev/null 2>&1