IndiPay
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 |
Requirements
- Laravel 5.6 to 12
- Guzzle 6 or 7
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 configcommand above and thencomposer update softon/indipay. Check thatcomposer.locknow listshttps://github.com/RenderbitTechnologies/indipayas the package source.For a private clone or SSH access, use
git@github.com:RenderbitTechnologies/indipay.gitas 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
VerifyCsrfTokenand add the response routes to its$exceptarray. 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:
gateway: the default gateway (one of the names in the table above). Read fromINDIPAY_GATEWAYand defaults toMocker, so set it in production.Indipay::gateway($name)throws anInvalidArgumentExceptionfor a name that is not supported (previously it silently kept the current gateway).testMode:truesends requests to the gateway’s sandbox. Read fromINDIPAY_TEST_MODEand defaults totrue, so setINDIPAY_TEST_MODE=falsein production.- One block per gateway (
ccavenue,payumoney,ebs,citrus,instamojo,zapakpay,paytm,mocker) for keys and return URLs. Most values read from.envvariables, such asINDIPAY_MERCHANT_ID,INDIPAY_WORKING_KEYorPAYTM_MERCHANT_KEY. ebs.hashAlgorithm: the hash algorithm your EBS payment page is set to:md5,sha1orsha512(case-insensitive). Read fromINDIPAY_EBS_HASH_ALGORITHMand defaults tomd5. It is used for both the requestsecure_hashand the responseSecureHashcheck, so it must match the algorithm selected for the page in your EBS account. Any other value makesEBSGatewaythrow anInvalidArgumentExceptionwhen it is created. If your publishedconfig/indipay.phppredates this option, the gateway falls back tomd5.payumoney.strictCallbackFieldsandcitrus.strictCallbackFields: whentrue,response()rejects a callback that leaves out a field covered by its hash. Read fromINDIPAY_STRICT_CALLBACK_FIELDSand defaults tofalse. See the note on absent fields below. If your publishedconfig/indipay.phppredates this option, it is off.remove_csrf_check: routes the published middleware lets through without a CSRF token, in addition to the app’s own exclusions ($except, orvalidateCsrfTokens(except: [...])on Laravel 11+).
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:
- 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.
- Compare the amount (and currency) with the amount stored for that order. Use your own record, never a value the user could have sent.
- Check the field that means “paid” for that gateway. A genuine callback can also report a failed or pending payment.
- 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:
- the status API says
STATUSisTXN_SUCCESS - the reply’s
ORDERIDis the one you asked about - the reply’s
MIDis your configuredMID(a reply that leaves outMIDis accepted) - the reply’s
TXNAMOUNTequalsamount, compared as numbers ('100.00'matches100)
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.