Skip to the content.

IndiPay

Tests PHP Laravel Guzzle PHPUnit License: MIT

A Laravel package for Indian payment gateways.

About this fork: This is a maintained fork of softon/indipay. The upstream package is no longer maintained and appears to be abandoned, so this fork carries on its development, including support for newer versions of PHP and Laravel. See Installation for how to install it in place of the upstream package.

Gateway Name to use in code Verify support
CCAvenue CCAvenue No
PayUMoney PayUMoney No
EBS EBS No
CitrusPay Citrus No
InstaMojo InstaMojo No
ZapakPay (Mobikwik) ZapakPay No
Paytm Paytm Yes
Mocker Mocker Yes

Laravel 4.2 version

Requirements

CI tests every Laravel release in that range, each on the oldest and newest PHP it has been verified with:

Laravel PHP Testbench PHPUnit
5.6, 5.7 7.1, 7.3 3.6, 3.7 7
5.8 7.1, 7.4 3.8 7
6 7.2, 8.0 4 8
7 7.2, 8.0 5 8 or 9
8 7.3, 8.1 6 9
9 8.0, 8.3 7 9
10 8.1 8 10
11 8.2–8.4 9 10 or 11
12 8.2–8.4 10 10 or 11

Installation

Step 1: Install the package from this fork with Composer. It is not published as a separate package on Packagist. The fork keeps the upstream package name, softon/indipay, so you point Composer at this repository and it uses that in place of the upstream release.

Add the repository to your app’s composer.json:

composer config repositories.indipay vcs https://github.com/RenderbitTechnologies/indipay

This adds the following to composer.json:

"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/RenderbitTechnologies/indipay"
    }
]

Then require the latest 4.x release:

composer require "softon/indipay:^4.0"

This installs the newest 4.x tag at or above v4.0.0. Read the changelog before upgrading from 3.x: 4.0.0 has breaking changes. To follow unreleased changes on the master branch instead, require softon/indipay:dev-master.

Laravel finds the service provider and the Indipay facade on its own.

If you already installed the upstream package, run the composer config command above and then composer update softon/indipay. Check that composer.lock now lists https://github.com/RenderbitTechnologies/indipay as the package source.

For a private clone or SSH access, use git@github.com:RenderbitTechnologies/indipay.git as the repository URL.

Step 2: Publish the config file and middleware.

php artisan vendor:publish --provider="Softon\Indipay\IndipayServiceProvider"

This creates config/indipay.php and app/Http/Middleware/VerifyCsrfMiddleware.php.

Step 3: Turn off CSRF checks on the gateway response routes. Gateways post back to your app without a CSRF token, so these routes must skip the check. List them under remove_csrf_check in config/indipay.php (default: indipay/response), then swap in the published middleware.

Laravel 10 and below: in app/Http/Kernel.php, change

App\Http\Middleware\VerifyCsrfToken::class,

to

App\Http\Middleware\VerifyCsrfMiddleware::class,

You may instead keep VerifyCsrfToken and add the response routes to its $except array. This needs no extra class.

Laravel 11 and above: there is no app/Http/Kernel.php. Swap the middleware in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(replace: [
        \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class => \App\Http\Middleware\VerifyCsrfMiddleware::class,
    ]);
})

You may instead skip the published middleware and list the response routes with $middleware->validateCsrfTokens(except: ['indipay/response']);

Configuration

Edit config/indipay.php:

Return URLs are route paths, passed through Laravel’s url() helper.

Usage

use Softon\Indipay\Facades\Indipay;

Start a payment with the default gateway and redirect the user:

// Required parameters differ for each gateway. See your gateway's manual.
$parameters = [
    'transaction_no' => '1233221223322',
    'amount' => '1200.00',
    'name' => 'Jon Doe',
    'email' => 'jon@doe.com',
];

$order = Indipay::prepare($parameters);
return Indipay::process($order);

The package checks that each gateway’s required parameters are present and throws Softon\Indipay\Exceptions\IndipayParametersMissingException if any are missing.

Use a gateway other than the default:

// CCAvenue / PayUMoney / EBS / Citrus / InstaMojo / ZapakPay / Paytm / Mocker
$order = Indipay::gateway('Paytm')->prepare($parameters);
return Indipay::process($order);

Handle the gateway’s response at the return URL set in the config file:

public function response(Request $request)
{
    // Default gateway
    $response = Indipay::response($request);

    // Any other gateway: use the same name as in the request
    $response = Indipay::gateway('NameOfGatewayUsedDuringRequest')->response($request);

    dd($response);
}

What Indipay::response checks and returns depends on the gateway:

Gateway How the response is checked Returns
CCAvenue Decrypted with your working key Array
PayUMoney Hash Array, or the string Hash Mismatch Error
Citrus Signature Array, or the string Hash Mismatch Error
ZapakPay Checksum Array, or the string Recieved Checksum Mismatch.
Paytm Checksum Array with a status key: success or failure
InstaMojo Fetched from the InstaMojo API Object, or false
EBS SecureHash Array, or the string Hash Mismatch Error
Mocker Not checked Array with a status key: success or failure

Absent fields count as empty. By default, for PayUMoney and Citrus, a field that is part of the hash and left out of the callback is treated as an empty string. If the gateway signed that field with an empty value, a callback without the key still passes the check, and the array you get back has no such key. PayUMoney joins the signed values with |, so a field that carried a value cannot be left out. Citrus joins them with no separator, so field boundaries are not protected: a callback can drop a non-empty field if the next field’s value is changed to the two values joined. Use array_key_exists('field', $response) to tell an absent field from an empty one.

To require every signed field, set INDIPAY_STRICT_CALLBACK_FIELDS=true (config keys payumoney.strictCallbackFields and citrus.strictCallbackFields). A callback that leaves out any of them is then rejected with Hash Mismatch Error, even if the value would have been empty. A field may still be present with an empty value. The fields are status, email, firstname, productinfo, amount, txnid and key for PayUMoney (the unused udf1 to udf5 fields may be left out, as PayU does), and TxId, TxStatus, amount, pgTxnNo, issuerRefNo, authIdCode, firstName, lastName, pgRespCode and addressZip for Citrus. Turn it on only if your gateway account always sends every one of them, because empty fields that are omitted will fail the check.

InstaMojo responses must be matched to your order. response() rejects (returns false) a callback whose payment_request_id or payment_id is not alphanumeric, then returns whatever the InstaMojo API reports for those IDs with success: true. It does not tie them to an order you created, or check the payment status. Before you fulfil an order, compare the returned payment_request->id, payment_request->amount and payment_request->payment->status (Credit) with your stored order. The payment object also carries the paid amount. These paths follow the InstaMojo 1.1 payment-request response and have not been checked against the live API.

Mocker responses are not checked. Anyone can post fake data to your response route, since it skips CSRF checks. Confirm the payment with the gateway (for example with verify()) before you fulfil an order.

Save the transaction details to your database before you send the user to the gateway, so you can check the payment status later.

After the response

A valid hash, checksum or API reply only proves the gateway sent the data. It does not prove the data is about the order you are fulfilling. A customer can pay for a cheap order and replay its genuine callback against your response route while their session points at an expensive one. Before you mark an order paid:

  1. Look up the order by the gateway’s transaction or order ID in the callback, not by anything in the session or the query string.
  2. Compare the amount (and currency) with the amount stored for that order. Use your own record, never a value the user could have sent.
  3. Check the field that means “paid” for that gateway. A genuine callback can also report a failed or pending payment.
  4. Make fulfilment idempotent. Accept a callback once: mark the order paid in a single atomic update and ignore repeats.

Only rely on signed fields. response() returns every field in the request ($request->all()), including fields the hash does not cover and any query-string parameters added to your return URL. For PayUMoney and Citrus the signed fields are listed above.

Gateway Order ID Amount Paid when
CCAvenue order_id amount order_status is Success
PayUMoney txnid amount status is success
Citrus TxId amount TxStatus is SUCCESS
Paytm ORDERID TXNAMOUNT status is success
EBS MerchantRefNo Amount ResponseCode is 0
ZapakPay orderId amount responseCode is 100
InstaMojo payment_request->id payment->amount payment->status is Credit
Mocker Not checked: use verify()    

The field names above are the ones the gateways document; check them against a real callback from your account. Your own order record is the source of truth for the order and the amount.

InstaMojo’s response() returns any payment request the API says exists for the IDs in the request, so its ID, amount and payment->status must be checked against your order as well.

public function response(Request $request)
{
    $response = Indipay::response($request);

    // PayUMoney is shown; use the fields from the table above for other gateways.
    if (! is_array($response) || ($response['status'] ?? null) !== 'success') {
        return redirect('/checkout')->with('error', 'Payment failed.');
    }

    DB::transaction(function () use ($response) {
        $order = Order::where('gateway_txn_id', $response['txnid'])->lockForUpdate()->firstOrFail();

        if ($order->isPaid()) {
            return; // A repeated callback: already fulfilled.
        }

        // Compare as numbers: '100.00' equals '100'.
        if ((float) $response['amount'] !== (float) $order->amount) {
            abort(400, 'Amount mismatch.');
        }

        $order->markPaid();
    });

    return redirect('/thank-you');
}

Payment Verification

Paytm and Mocker can look up a payment’s status, for example when it is still pending. Calling verify() on any other gateway throws Softon\Indipay\Exceptions\UnsupportedOperationException. A custom gateway opts in by implementing Softon\Indipay\Gateways\VerifiesPayments:

// Paytm
$order = Indipay::gateway('Paytm')->verify([
    'ORDERID' => '3322344231223',
    'amount' => '1200.00',
]);

// Mocker
$order = Indipay::gateway('Mocker')->verify([
    'transaction_no' => '3322344231223',
    'amount' => '1200.00',
]);

if ($order['status'] === 'success') {
    // Paid in full: fulfil the order.
}

Pass the amount you expect, taken from your own records, not from the gateway’s callback. Each result carries a status of success or failure, plus the fields the status API returned, if any.

Paytm reports success only when all of these hold:

If the status call fails (a network or TLS error, a non-200 reply, or a body that is not JSON with a STATUS), Paytm’s verify() returns ['status' => 'failure'] instead of throwing. It returns false when ORDERID or amount is missing. The call checks Paytm’s TLS certificate.

Paytm’s verify() requires amount. Without it, verify() returns false and makes no status call. (In 3.x it raised a deprecation notice and accepted any amount.)

Mocker’s verify() throws IndipayParametersMissingException without transaction_no and amount, and reports success only when the reply’s amount matches.

Testing

composer install
composer test

The repository does not commit a composer.lock, so composer install picks the newest versions that work with your PHP: Laravel 12 on PHP 8.2 or later, and Laravel 10 on PHP 8.1.

Tests use Orchestra Testbench and make no network calls.

To run the suite against Laravel 9 or older, install the matching Testbench and PHPUnit from the table under Requirements, run .github/scripts/legacy-data-providers.sh (it rewrites the tests’ #[DataProvider] attributes, which PHPUnit before 10 ignores, so use a throwaway checkout) and run vendor/bin/phpunit --configuration phpunit.legacy.xml.

License

MIT. See LICENSE.