Integrating PaytmJS Checkout in Laravel and Vue.js
In this comprehensive guide, we'll walk through the process of integrating PaytmJS Checkout into a Laravel backend with a Vue.js frontend. This integration will enable your application to accept payments securely through the Paytm payment gateway.
Prerequisites
Before we begin, ensure you have the following:
- Laravel project (8.x or higher recommended)
- Vue.js set up within Laravel or as a separate application
- Paytm merchant account with:
- Merchant ID
- Merchant Key
- Website name
- Industry type
- Composer installed
- npm or yarn installed
Step 1: Install Paytm Payment Package
First, we need to install a Laravel package to handle Paytm payment gateway integration. Let's use the popular anandsiddharth/laravel-paytm-wallet
package:
composer require anandsiddharth/laravel-paytm-wallet
Step 2: Publish Package Configuration
Publish the configuration file:
php artisan vendor:publish --provider="Anand\LaravelPaytmWallet\PaytmWalletServiceProvider"
Step 3: Configure Paytm Credentials
Open the configuration file at config/paytm.php
and update it with your Paytm credentials:
<?php
return [
'environment' => env('PAYTM_ENVIRONMENT', 'production'), // values: 'production', 'local'
'merchant_id' => env('PAYTM_MERCHANT_ID', ''),
'merchant_key' => env('PAYTM_MERCHANT_KEY', ''),
'merchant_website' => env('PAYTM_WEBSITE', ''),
'channel' => env('PAYTM_CHANNEL', 'WEB'),
'industry_type' => env('PAYTM_INDUSTRY_TYPE', 'Retail'),
];
Step 4: Add Environment Variables
Update your .env
file with your Paytm credentials:
PAYTM_ENVIRONMENT=local
PAYTM_MERCHANT_ID=YOUR_MERCHANT_ID
PAYTM_MERCHANT_KEY=YOUR_MERCHANT_KEY
PAYTM_WEBSITE=WEBSTAGING
PAYTM_CHANNEL=WEB
PAYTM_INDUSTRY_TYPE=Retail
Creating the Payment API
Step 1: Create Payment Model and Migration
Generate a payment model and migration:
php artisan make:model Payment -m
Update the migration file:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePaymentsTable extends Migration
{
public function up()
{
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->string('order_id')->unique();
$table->string('user_id')->nullable();
$table->decimal('amount', 10, 2);
$table->string('transaction_id')->nullable();
$table->string('status')->default('pending');
$table->json('payment_data')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('payments');
}
}
Run the migration:
php artisan migrate
Step 2: Create Payment Controller
Generate a payment controller:
php artisan make:controller PaymentController
Update the controller with the following code:
<?php
namespace App\Http\Controllers;
use App\Models\Payment;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Anand\LaravelPaytmWallet\Facades\PaytmWallet;
class PaymentController extends Controller
{
/**
* Initiate a payment
*/
public function initiate(Request $request)
{
$request->validate([
'amount' => 'required|numeric|min:1',
'user_id' => 'required',
]);
// Generate a unique order ID
$orderId = 'ORDER' . time() . Str::random(6);
// Store payment info in database
$payment = Payment::create([
'order_id' => $orderId,
'user_id' => $request->user_id,
'amount' => $request->amount,
'status' => 'pending',
]);
// Get payment initiation data
$paymentData = PaytmWallet::with('receive')
->setOrderId($orderId)
->setAmount($request->amount)
->setCustomerId($request->user_id)
->getTransactionToken();
// Return checkout data for frontend
return response()->json([
'order_id' => $orderId,
'amount' => $request->amount,
'token' => $paymentData['body']['txnToken'],
'merchant_id' => config('paytm.merchant_id'),
'callback_url' => route('paytm.callback'),
]);
}
/**
* Handle payment callback
*/
public function callback(Request $request)
{
$transaction = PaytmWallet::with('receive');
$response = $transaction->response();
$order_id = $transaction->getOrderId();
// Get payment record
$payment = Payment::where('order_id', $order_id)->first();
if($transaction->isSuccessful()) {
// Payment successful
$payment->update([
'status' => 'completed',
'transaction_id' => $response['TXNID'],
'payment_data' => json_encode($response),
]);
return redirect()->route('payment.success')->with('success', 'Payment successful');
} else if($transaction->isFailed()) {
// Payment failed
$payment->update([
'status' => 'failed',
'payment_data' => json_encode($response),
]);
return redirect()->route('payment.failed')->with('error', 'Payment failed');
} else if($transaction->isOpen()) {
// Payment pending
$payment->update([
'status' => 'pending',
'payment_data' => json_encode($response),
]);
return redirect()->route('payment.pending')->with('info', 'Payment pending');
}
// Fallback
return redirect()->route('payment.failed')->with('error', 'Something went wrong');
}
/**
* Check payment status
*/
public function status($orderId)
{
$payment = Payment::where('order_id', $orderId)->first();
if(!$payment) {
return response()->json(['error' => 'Payment not found'], 404);
}
return response()->json([
'status' => $payment->status,
'order_id' => $payment->order_id,
'amount' => $payment->amount,
'transaction_id' => $payment->transaction_id,
]);
}
}
Step 3: Define Routes
Add the following routes to your routes file (routes/api.php
):
<?php
use App\Http\Controllers\PaymentController;
// Payment routes
Route::post('/payment/initiate', [PaymentController::class, 'initiate']);
Route::post('/payment/callback', [PaymentController::class, 'callback'])->name('paytm.callback');
Route::get('/payment/status/{orderId}', [PaymentController::class, 'status']);
// Result pages
Route::view('/payment/success', 'payment.success')->name('payment.success');
Route::view('/payment/failed', 'payment.failed')->name('payment.failed');
Route::view('/payment/pending', 'payment.pending')->name('payment.pending');
Implementing Vue.js Frontend
Step 1: Create Payment Component
Create a new Vue component for handling Paytm payments:
# If using Laravel Mix
touch resources/js/components/PaytmCheckout.vue
Add the following code to the component:
<template>
<div class="paytm-checkout">
<h2>Payment Details</h2>
<div class="form-group">
<label for="amount">Amount</label>
<input
type="number"
id="amount"
v-model="amount"
class="form-control"
:disabled="loading"
>
</div>
<button
@click="initiatePayment"
class="btn btn-primary"
:disabled="loading || !amount"
>
{{ loading ? 'Processing...' : 'Pay Now' }}
</button>
<div v-if="error" class="alert alert-danger mt-3">
{{ error }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
amount: 100,
userId: 'USER123', // This should be dynamically set from your auth system
loading: false,
error: null,
checkoutConfig: null
};
},
methods: {
async initiatePayment() {
this.loading = true;
this.error = null;
try {
// Step 1: Get payment token from backend
const response = await axios.post('/api/payment/initiate', {
amount: this.amount,
user_id: this.userId
});
// Step 2: Store payment config
this.checkoutConfig = {
orderId: response.data.order_id,
token: response.data.token,
amount: response.data.amount,
merchantId: response.data.merchant_id,
callbackUrl: response.data.callback_url
};
// Step 3: Load Paytm JS
await this.loadPaytmScript();
// Step 4: Open checkout
this.openPaytmCheckout();
} catch (error) {
console.error('Payment initiation failed:', error);
this.error = error.response?.data?.message || 'Failed to initiate payment. Please try again.';
} finally {
this.loading = false;
}
},
loadPaytmScript() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://securegw.paytm.in/merchantpgpui/checkoutjs/merchants/' + this.checkoutConfig.merchantId + '.js';
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
},
openPaytmCheckout() {
if (typeof window.Paytm === 'undefined') {
this.error = 'Failed to load Paytm checkout. Please try again.';
return;
}
// Configure checkout
const config = {
root: "",
flow: "DEFAULT",
data: {
orderId: this.checkoutConfig.orderId,
token: this.checkoutConfig.token,
tokenType: "TXN_TOKEN",
amount: this.checkoutConfig.amount
},
merchant: {
mid: this.checkoutConfig.merchantId,
redirect: true
},
handler: {
notifyMerchant: (eventName, data) => {
console.log("notifyMerchant handler function called");
console.log("eventName => ", eventName);
console.log("data => ", data);
if (eventName === 'APP_CLOSED') {
// User closed the payment page
this.checkPaymentStatus();
}
}
}
};
// Initialize and invoke Paytm checkout
window.Paytm.CheckoutJS.init(config)
.then(() => {
window.Paytm.CheckoutJS.invoke();
})
.catch(error => {
console.error("Error in CheckoutJS init", error);
this.error = 'Failed to initialize payment gateway. Please try again.';
});
},
async checkPaymentStatus() {
if (!this.checkoutConfig?.orderId) return;
try {
const statusResponse = await axios.get(`/api/payment/status/${this.checkoutConfig.orderId}`);
const status = statusResponse.data.status;
if (status === 'completed') {
alert('Payment successful!');
// Redirect or update UI as needed
} else if (status === 'failed') {
this.error = 'Payment failed. Please try again.';
} else if (status === 'pending') {
this.error = 'Payment is pending. We will update you once confirmed.';
}
} catch (error) {
console.error('Failed to check payment status:', error);
}
}
}
};
</script>
<style scoped>
.paytm-checkout {
max-width: 500px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-group {
margin-bottom: 15px;
}
.form-control {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.btn-primary {
background-color: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
}
.btn-primary:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>
Step 2: Register Component in Your App
Import and register the component in your main Vue application file:
// resources/js/app.js
import { createApp } from 'vue';
import PaytmCheckout from './components/PaytmCheckout.vue';
const app = createApp({});
app.component('paytm-checkout', PaytmCheckout);
app.mount('#app');
Step 3: Add Component to Your View
Add the component to a Laravel Blade view:
<!-- resources/views/checkout.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div id="app">
<paytm-checkout></paytm-checkout>
</div>
</div>
</div>
</div>
@endsection
Add route for the checkout page:
// routes/web.php
Route::get('/checkout', function () {
return view('checkout');
});
Handling Payment Callbacks
When a payment is completed, Paytm will redirect to the callback URL we provided. This callback is handled by the callback
method in our PaymentController
.
Let's create simple views for payment results:
mkdir -p resources/views/payment
touch resources/views/payment/success.blade.php
touch resources/views/payment/failed.blade.php
touch resources/views/payment/pending.blade.php
Success Page
<!-- resources/views/payment/success.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<div class="alert alert-success">
<h2>Payment Successful!</h2>
<p>Your transaction has been completed successfully.</p>
<a href="/" class="btn btn-primary">Return to Home</a>
</div>
</div>
@endsection
Failed Page
<!-- resources/views/payment/failed.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<div class="alert alert-danger">
<h2>Payment Failed</h2>
<p>Your transaction could not be completed. Please try again.</p>
<a href="/checkout" class="btn btn-primary">Try Again</a>
</div>
</div>
@endsection
Pending Page
<!-- resources/views/payment/pending.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<div class="alert alert-warning">
<h2>Payment Pending</h2>
<p>Your transaction is being processed. We will notify you once it's complete.</p>
<a href="/" class="btn btn-primary">Return to Home</a>
</div>
</div>
@endsection
Testing the Integration
To test your Paytm integration:
- Make sure your application is running
- Visit the checkout page at
/checkout
- Enter an amount and click "Pay Now"
- You'll be redirected to the Paytm payment page
- For testing, use Paytm's test credentials:
- Mobile Number: 7777777777
- OTP: Any 6 digits
- Debit Card: Any valid card format with CVV 123
- Complete the payment
- You'll be redirected back to your application
Best Practices and Security Considerations
- Environment Variables: Always store sensitive credentials in environment variables, never hardcode them.
- Data Validation: Validate all inputs, especially amount and user data.
- HTTPS: Use HTTPS for all payment-related routes.
- Testing Mode: Use Paytm's test environment before going live.
- Error Handling: Implement comprehensive error handling for payment failures.
- Idempotency: Ensure that the same payment can't be processed twice.
- Logging: Implement logging for payment activities for audit purposes.
- Webhooks: Consider implementing additional webhook support for asynchronous payment updates.
Common Issues:
Callback URL Issues:
- Make sure your callback URL is registered in your Paytm merchant dashboard
- For local testing, you may need to use a service like ngrok
CSRF Token Mismatch:
- Add an exception for the callback route in your
VerifyCsrfToken
middleware:
protected $except = [
'payment/callback',
];
Transaction Token Error:
- Verify your merchant ID and key
- Check that you're using the correct environment (test/production)
Payment Status Not Updating:
- Check your database connection
- Verify that the order ID is being properly tracked
Paytm JS Not Loading:
- Check browser console for errors
- Verify merchant ID in the JS URL
Debug Tips:
- Check the browser console for JavaScript errors
- Monitor Laravel logs for backend errors
- Use Paytm's dashboard to track transaction status
- For detailed debugging, implement more extensive logging in your payment controller
Conclusion
You've now successfully integrated PaytmJS Checkout into your Laravel and Vue.js application. This setup provides a secure and seamless payment experience for your users.
Remember to thoroughly test the integration in Paytm's testing environment before moving to production. Also, make sure to keep your Paytm credentials secure and follow best practices for handling sensitive payment information.