Nothing halts an e-commerce business faster than seeing the dreaded razorpay payment gateway integration failed notification or an unhandled phonepe checkout error right when a customer hits “Pay Now”. The customer sees money deducted from their bank account, but your store marks the order as “Pending” or “Failed”—leading to angry support tickets and abandoned carts.
Direct Answer (Why Payment Gateway Integrations Fail & How to Fix Them):
The top causes of Razorpay and PhonePe checkout failures in modern web applications include: 1) Webhook Signature Verification Failures caused by frameworks parsing the raw request body before HMAC SHA256 validation; 2) PhonePe X-VERIFY Checksum Mismatches due to trailing slashes or salt index formatting; 3) UPI Intent Deep-Link Blocks within in-app social browsers (Instagram and WhatsApp webviews); 4) Auto-Capture Omission leaving payments in an “Authorized” state that auto-refunds in 5 days; and 5) Idempotency Failures creating duplicate orders during network timeouts.
At AMSIT, we architect high-throughput custom e-commerce engines, mobile apps, and enterprise payment integrations. In this forensic engineering guide, we synthesize verified community fixes from Reddit (r/developersIndia), StackOverflow, and merchant developer forums to solve the 5 most frustrating payment gateway bugs once and for all.
Table of Contents
- Error 1: Webhook Signature Verification Failed (The Raw Body Pitfall)
- Error 2: PhonePe X-VERIFY Checksum Mismatch (BAD_REQUEST / CHECKSUM_FAILED)
- Error 3: UPI Intent Not Opening in Mobile In-App Browsers (Instagram/WhatsApp)
- Error 4: Money Debited but Order Pending (Authorized vs. Captured State)
- Error 5: Duplicate Orders and Race Conditions on Double-Clicks
- Payment Gateway Architectural Hardening Checklist
- Frequently Asked Questions (FAQ)
Error 1: Webhook Signature Verification Failed (The Raw Body Pitfall)
As documented extensively on StackOverflow and Reddit, by far the single most common cause of webhook signature verification failed razorpay errors is validating the signature against a parsed JSON object rather than the exact raw incoming byte stream.
When Razorpay signs a webhook payload, it calculates an HMAC SHA-256 hash using your secret key and the exact byte-for-byte JSON payload. Modern web frameworks (such as Express.js, Fastify, Next.js, and Flask) automatically parse incoming request bodies into JavaScript/Python objects via express.json() or json.loads().
When you re-stringify that parsed object (JSON.stringify(req.body)) to verify the signature, whitespace, property orders, and escaped unicode characters change subtly. The calculated hash fails to match the X-Razorpay-Signature header, and your server responds with 400 Bad Request.
The Developer Solution (Node.js / Express): Capture the raw unparsed buffer before any body parsing middleware touches the request:
// Capture raw body specifically for the Razorpay Webhook route
app.post('/api/razorpay-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const crypto = require('crypto');
const secret = process.env.RAZORPAY_WEBHOOK_SECRET;
const signature = req.headers['x-razorpay-signature'];
// Verify HMAC SHA256 using the RAW buffer
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(req.body) // req.body must be the unparsed Buffer!
.digest('hex');
if (expectedSignature === signature) {
const event = JSON.parse(req.body.toString());
console.log('Verified Webhook Event:', event.event);
// Process order fulfillment...
return res.status(200).json({ status: 'ok' });
} else {
console.error('Signature Mismatch!');
return res.status(400).json({ error: 'Invalid signature' });
}
});
Error 2: PhonePe X-VERIFY Checksum Mismatch (BAD_REQUEST / CHECKSUM_FAILED)
PhonePe’s Standard PG API requires sending an X-VERIFY header containing an encrypted checksum on every request. Developers across developer forums frequently encounter 400 BAD_REQUEST or CHECKSUM_FAILED errors due to three subtle formatting flaws:
- Incorrect Checksum Formula: PhonePe requires hashing the Base64-encoded payload concatenated with the API endpoint path and your Salt Key, followed by
###and your Salt Index:
SHA256(base64Payload + "/pg/v1/pay" + saltKey) + "###" + saltIndex - Trailing Slashes in the Path: Passing
/pg/v1/pay/instead of/pg/v1/paybreaks the SHA256 digest entirely. - Wrong Salt Index: If your PhonePe merchant console provides Salt Key with Index
1, appending###2will instantly reject the API payload.
The Verified PHP Implementation:
<?php
// Working PhonePe Checksum Generation Protocol
$payload = [
'merchantId' => 'MERCHANTUAT',
'merchantTransactionId' => 'TXN_' . time(),
'merchantUserId' => 'CUST_101',
'amount' => 99900, // Amount in paise (₹999.00)
'redirectUrl' => 'https://example.com/payment-callback',
'redirectMode' => 'POST',
'callbackUrl' => 'https://example.com/api/phonepe-webhook',
'paymentInstrument' => ['type' => 'PAY_PAGE']
];
$saltKey = '099eb0cd-02cf-4e2a-8aca-3e6c6aff0399'; // Your Salt Key
$saltIndex = '1'; // Your Salt Index
$base64Payload = base64_encode(json_encode($payload));
$endpoint = '/pg/v1/pay'; // Strict endpoint without trailing slash
$hash = hash('sha256', $base64Payload . $endpoint . $saltKey);
$xVerify = $hash . '###' . $saltIndex;
// Headers to send with cURL
$headers = [
'Content-Type: application/json',
'X-VERIFY: ' . $xVerify,
'accept: application/json'
];
?>
Error 3: UPI Intent Not Opening in Mobile In-App Browsers (Instagram/WhatsApp)
If you run paid advertising on Instagram, Facebook, or distribute product links via WhatsApp, over 80% of your mobile traffic opens inside an in-app webview browser rather than standalone Safari or Chrome.
When your checkout triggers a UPI Intent call (e.g., upi://pay?pa=merchant@upi... or invoking Google Pay / PhonePe directly), the in-app webview security sandbox blocks deep-link intent requests for security reasons. To the user, tapping “Pay with GPay” or “Pay with PhonePe” produces no reaction whatsoever, resulting in an immediate abandoned cart.
The Engineering Solution:
- Detect In-App Webviews: Inspect the browser’s
navigator.userAgentfor strings likeFBAN,FBAV,Instagram, orWhatsApp. - Provide Adaptive Fallback: If an in-app browser is detected, do not invoke raw intent deep-links. Instead, present a dynamic UPI Dynamic QR Code that the customer can screenshot and pay via any app, or render payment gateway hosted checkout pages that handle webview fallbacks natively.
Error 4: Money Debited but Order Pending (Authorized vs. Captured State)
In payment gateway architecture, a transaction progresses through two distinct states:
- Authorization: The customer authenticates with their bank (OTP / UPI PIN), and the funds are reserved by the issuing bank.
- Capture: The merchant server acknowledges and claims the funds, settling them into your merchant account.
If you create a Razorpay order without setting payment_capture: 1 (or leave auto-capture disabled in your WooCommerce or custom backend settings), the payment remains in the “Authorized” state. Razorpay will automatically reverse and refund the money to the customer within 5 business days if uncaptured.
The Fix: Always ensure "payment_capture": 1 is explicitly specified when creating the Razorpay order entity via API:
// Enforce Automatic Capture on Order Creation
const order = await razorpay.orders.create({
amount: 99900, // in paise
currency: 'INR',
receipt: 'order_rcptid_11',
payment_capture: 1 // 1 = Auto Capture, 0 = Manual Capture
});
Error 5: Duplicate Orders and Race Conditions on Double-Clicks
When mobile customers experience slow 4G connections, they frequently tap the “Confirm & Pay” button multiple times. If your frontend lacks debouncing or your backend lacks idempotency locks, your server spawns multiple distinct gateway order IDs for the same shopping cart.
The customer pays one order, but your database webhook receives a callback for a different order ID—leaving the order marked as “Unpaid”.
The Solution: Enforce Idempotency Keys. Before creating a gateway order, assign a unique deterministic session hash (e.g., cart_id + user_id + timestamp_hour) and disable the checkout submit button on first click with an active loading spinner.
Payment Gateway Architectural Hardening Checklist
Before launching any e-commerce application to production, ensure your technical stack satisfies these 5 operational benchmarks:
- Dual Reconciliation: Never rely exclusively on client-side frontend redirect callbacks (which fail if the customer closes the browser tab early). Always listen to asynchronous server webhooks as your primary source of truth.
- SSL TLS 1.3 Enforcement: Both Razorpay and PhonePe reject webhooks sent to servers running outdated TLS 1.0 or self-signed SSL certificates.
- Cloudflare WAF Whitelisting: Add Cloudflare WAF bypass rules for official Razorpay webhook IPs (e.g.,
52.66.75.174,52.66.103.172) so edge firewalls don’t block payment callbacks. - Webhook Retry Resilience: Return a fast
200 OKresponse within 2 seconds of receiving a webhook, and process heavy inventory or email routines asynchronously in a background job queue.
Frequently Asked Questions (FAQ)
Why did Razorpay deduct money from my customer, but the dashboard shows failed?
This occurs when the customer’s bank approved the debit, but the gateway did not receive authorization confirmation before the session timed out. Razorpay’s automated Late Authorization system reconciles these payments within 24 hours, either capturing the order or initiating an automatic refund.
Can I test Razorpay webhooks on localhost?
No. Gateway servers cannot reach private localhost URLs. You must use secure HTTPS tunneling tools like ngrok or Cloudflare Tunnels (e.g., https://xyz.ngrok-free.app/webhook) and register that URL in your gateway test settings.
Is PhonePe better than Razorpay for Indian UPI payments?
Both gateways offer industry-leading UPI success rates (typically 92%+). Razorpay provides superior developer documentation and out-of-the-box WooCommerce/Shopify plugins, while PhonePe offers competitive transaction fees and deep integration with the PhonePe merchant ecosystem.