Open source · Laravel · Payments
Laravel Razorpay Billing: Cashier-Style Subscriptions, UPI AutoPay and GST Invoices
If you run a SaaS in India on Laravel, you’ve probably wished Laravel Cashier worked with Razorpay. I built laravel-razorpay-billing, a free, MIT-licensed package that brings the same fluent subscription API to Razorpay. It also handles the parts specific to Indian recurring payments: UPI AutoPay limits, e-mandates and GST.
- Install
composer require shasha1589/laravel-razorpay-billing - SupportsLaravel 12 & 13, PHP 8.2+
- LicenseMIT, free forever
- DependenciesNone beyond Laravel
Why a Razorpay package for Laravel?
Laravel Cashier is excellent, but its official packages cover Stripe and Paddle. Indian businesses mostly bill through Razorpay, and recurring payments in India come with rules that a generic integration doesn’t handle well:
- UPI AutoPay limits. Razorpay documents a default ₹15,000 cap per UPI AutoPay debit. Pricier plans need card or bank e-mandates.
- RBI e-mandate rules. High-value card debits need extra customer authentication, and customers expect a reminder before each debit.
- Mandates customers can cancel themselves. A customer can pause or cancel a UPI mandate from their UPI app, and your app needs to find out.
- GST. Invoices need CGST and SGST or IGST, depending on where the buyer is, and numbering that restarts every financial year.
Writing all of this by hand for every project is slow and easy to get wrong, especially webhook handling. This package wraps it in an API Cashier users already know.
Features at a glance
- Fluent subscriptions:
newSubscription(),subscribed(),onTrial(),swap(),cancel(),resume() - Plans defined in config and synced to Razorpay with one Artisan command
- UPI AutoPay limit checks, card and bank e-mandates, and mandate cancel/pause events
- Webhooks with signature verification, duplicate protection and safe handling of out-of-order events
- A Laravel event for every billing change, such as
SubscriptionChargedandSubscriptionPastDue - One-time payments through Razorpay orders, plus full and partial refunds
- Optional GST invoices with gap-free numbering
- Works with any billable model: users, teams or organisations
- Tested with Pest and analysed with Larastan at max level. No Razorpay SDK needed, so
Http::fake()works in your tests
Installation
composer require shasha1589/laravel-razorpay-billing
php artisan razorpay:install
php artisan migrate
Add your Razorpay keys to .env (use rzp_test_ keys while developing):
RAZORPAY_KEY_ID=rzp_test_xxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxx
RAZORPAY_WEBHOOK_SECRET=xxxxxxxx
Step 1: Define your plans
Plans live in config/razorpay-billing.php. Amounts are always whole numbers in paise, so there are no floating-point rounding errors in money.
'plans' => [
'pro-monthly' => [
'name' => 'Pro',
'period' => 'monthly',
'interval' => 1,
'amount' => 1999900, // ₹19,999
'methods' => ['card', 'emandate', 'upi'],
'total_count' => 120,
],
],
Create them in Razorpay with one command. It’s safe to run again and again. Razorpay plans can’t be edited, so if you change a price, the command reports a conflict instead of quietly creating duplicates.
php artisan razorpay:sync-plans
Step 2: Make a model billable
use Shasha1589\RazorpayBilling\Billable;
class Organization extends Model
{
use Billable;
}
Step 3: Start a subscription
$checkout = $org->newSubscription('default', 'pro-monthly')
->trialDays(14)
->methods(['card', 'emandate'])
->create();
create() sets up the subscription in Razorpay and returns a Checkout object. Hand it to the bundled JavaScript helper, which opens Razorpay Checkout and confirms the payment with your server:
<script src="{{ asset('vendor/razorpay-billing/razorpay-checkout.js') }}"></script>
<script>
RazorpayBilling.checkout(@json($checkout))
.then(() => window.location = '/billing')
.catch((error) => { if (! error.dismissed) alert(error.message); });
</script>
It works the same way with Inertia and React: pass $checkout as a prop.
Step 4: Check access anywhere
if ($org->subscribed()) {
// Pro features
}
$org->onTrial();
$org->subscription()->pastDue(); // a charge failed and Razorpay is retrying
$org->subscription()->onGracePeriod(); // cancelled, but paid until the cycle ends
Managing subscriptions
$subscription = $org->subscription();
$subscription->swap('pro-yearly'); // at the end of the billing cycle
$subscription->updateQuantity(5, 'now');
$subscription->cancel(); // stays active until the period ends
$subscription->pause();
$subscription->resume();
$subscription->addOn(30000, 'Extra seat'); // one-time ₹300 on the next invoice
The package checks Razorpay’s rules before calling the API. For example, Razorpay can’t change the plan of a UPI or e-mandate subscription, so you get a clear exception instead of a confusing API error.
Webhooks done right
Webhooks are the real source of truth in any payment integration, and they’re where most bugs hide. Point your Razorpay webhook at https://your-app.com/razorpay/webhook and run a queue worker. The package then:
- Verifies every request. It checks the signature against the raw request body with a constant-time comparison.
- Processes each event once. Every event is stored by its Razorpay event ID, so a repeated delivery does nothing.
- Handles events in any order. A stale “payment pending” event can’t undo a newer “active” status, and a cancelled subscription is never revived.
- Answers fast. The work happens in a queued job, so Razorpay gets a quick 200.
You react with plain Laravel listeners:
| Razorpay webhook | Laravel event |
|---|---|
subscription.activated |
SubscriptionActivated |
subscription.charged |
SubscriptionCharged |
subscription.pending |
SubscriptionPastDue |
subscription.cancelled |
SubscriptionCancelled |
token.cancelled |
MandateCancelled |
refund.processed |
RefundProcessed |
Each event fires once per real change, whether that change came from a webhook, the checkout confirmation or your own API call. You won’t send a customer the same “welcome” email twice.
UPI AutoPay and pre-debit reminders
If a checkout is UPI-only and the amount is over your UPI AutoPay limit (₹15,000 by default, configurable), the package refuses it up front instead of letting it fail at the bank. When other methods are allowed too, UPI is simply left out of that checkout.
For pre-debit notifications, schedule one command hourly. It fires an UpcomingCharge event, by default 48 hours before each debit, so you can send your own reminder:
Schedule::command('razorpay:upcoming-charges')->hourly();
Schedule::command('razorpay:sync')->hourly(); // catches any missed webhooks
One-time payments and refunds
$checkout = $org->charge(49900, ['description' => 'E-book']); // ₹499
$payment->refund(); // full refund
$payment->refund(10000); // partial: ₹100
GST invoices (optional)
Turn on the invoice module and each captured payment gets a GST invoice:
- Numbers like
INV/2026-27/000123that never skip and restart every financial year (April to March, IST) - CGST + SGST for buyers in your state, IGST for other states, and zero-rated invoices for exports
- Tax-exclusive pricing: GST is added on top before the amount goes to Razorpay
- A small interface to plug in any PDF renderer, such as dompdf
The GST module does the arithmetic. It isn’t tax advice, so confirm the invoice format and tax treatment with your chartered accountant.
Testing your billing code
Everything goes through Laravel’s HTTP client, so you never hit Razorpay in your test suite:
Http::fake([
'api.razorpay.com/v1/subscriptions' => Http::response(['id' => 'sub_test1234567890', 'status' => 'created']),
]);
Event::fake([SubscriptionActivated::class]);
Frequently asked questions
Does Laravel Cashier support Razorpay?
No. Official Cashier packages cover Stripe and Paddle. This package gives Razorpay a Cashier-style API with the same method names, such as newSubscription, subscribed and cancel.
Which Laravel versions are supported?
Laravel 12 (PHP 8.2+) and Laravel 13 (PHP 8.3+). CI runs every combination of PHP 8.2–8.5 on SQLite, MySQL, MariaDB and PostgreSQL.
Do I need the official Razorpay PHP SDK?
No. The package uses Laravel’s own HTTP client and adds no extra dependencies.
Does it store card or UPI details?
No. Razorpay holds payment credentials. The package stores only Razorpay IDs, statuses, amounts and payment methods.
Is it production-ready?
It’s at version 0.x and fully tested, but the API may still change between minor versions. It will be marked 1.0 after running in production for a full billing cycle. Feedback and pull requests are welcome.
Get started
The package is free and open source under the MIT licence. Star it on GitHub, try it in your next Laravel SaaS, and open an issue if Razorpay does something it doesn’t handle yet.
