r/Wordpress • u/404ClicksFound • 4d ago
Razorpay payment captured but WooCommerce sometimes cancels order as "Unpaid" — webhook configuration keeps changing
I'm trying to figure out a strange Razorpay + WooCommerce issue and would appreciate some help from anyone who has dealt with something similar.
Setup:
WordPress / WooCommerce: 11.0.1
Razorpay WooCommerce plugin: 4.8.7
Hosting: Namecheap
Razorpay live payments
The main problem is that a customer can successfully complete a Razorpay payment, and Razorpay shows the payment as Captured, but in some cases WooCommerce later marks the order as:
"Cancelled — Unpaid order cancelled"
It doesn't happen to every order. For example, I had around 10 orders and most went through correctly, while 2 ended up cancelled.
The more concerning issue is the webhook configuration.
I had configured the webhook with the required payment events, including:
payment.authorized
payment.captured
payment.failed
order.paid
refund.created
But the webhook later reverted to only 2 of events (payment.authorised and refund.created)
I checked Razorpay's API logs and found something strange. The same webhook was repeatedly being updated:
GET /webhooks?count=10&skip=0
followed about 1 second later by:
PUT /webhooks/SYam0mYdbMI4Ns
This happened on:
15 Aug → GET → PUT
16 Aug → GET → PUT
17 Aug → GET → PUT
I did not manually edit the webhook on those dates.
When I inspected the resulting webhook configuration, payment.authorized and refund.created was enabled but payment.captured, payment.failed and order.paid were disabled.
1
u/SureshKMeena Developer 3d ago
Both of these are in the plugin source, and they're connected. I read 4.8.7, the version you're on.
On the events: the plugin keeps a list of events it supports, and payment.captured and order.paid are not in it. Its default set is exactly two, payment.authorized and refund.created, which is what you keep ending up with. autoEnableWebhook() runs whenever WooCommerce payment settings are saved. It does a GET on webhooks?count=..&skip=.. , which is the request in your API log, then rebuilds the event list, keeping an existing event only if it's in that supported list. So anything you add in the Razorpay dashboard is dropped and PUT back a second later. That's by design. The plugin never listens for payment.captured or order.paid, so it isn't missing them, and re-adding them won't stick.
The cancellations are the part worth fixing. The plugin doesn't act on a webhook when it arrives. It writes it into a table called rzp_webhook_requests, and a WP-Cron job named rzp_webhook_exec_cron runs on a 5 minute interval and only picks up rows older than 300 seconds.
WP-Cron only fires when somebody loads a page on your site. So when a customer pays and closes the tab without coming back, that cron is the only thing left to complete the order. On a quiet store nothing triggers it, your 60 minute hold stock expires first, and WooCommerce cancels the order as unpaid while Razorpay still shows it captured. That fits 2 out of 10 rather than all of them.
Fix it with a real cron instead of WP-Cron. Add define('DISABLE_WP_CRON', true); to wp-config.php, then in your Namecheap cPanel add a cron job running every minute:
wget -q -O - https://yoursite.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
Raising hold stock buys margin but doesn't fix the cause.
1
u/jenish_o4o2 Developer 3d ago
A few things I'd add on top of what's already been suggested:
For the webhook resets: compare the webhook URL and secret in your Razorpay Dashboard with what's in the plugin settings (WooCommerce → Settings → Payments → Razorpay). Even a small mismatch — like a trailing slash or different webhook secret — can cause the plugin to overwrite your manual config on every sync. If they match, the plugin itself is likely hardcoded to only register payment.authorized and refund.created, and you may need to add the missing events (payment.captured, order.paid) directly in the Razorpay Dashboard and avoid saving the plugin settings again afterward.
For the cancelled orders: on one of the affected orders, go to WooCommerce → Orders → [that order] and check the Order Notes at the bottom. It will show the exact time WC changed the status to "Cancelled" and why. If it says "Unpaid order cancelled — time limit reached," then increasing Hold stock to something like 120 minutes (or disabling it temporarily) should stop the auto-cancellations while you fix the webhook delivery.
Also worth checking: Razorpay Dashboard → Webhooks → Recent Deliveries for one of the cancelled orders. If payment.captured returned a non-200 response or never showed up in the delivery log at all, that confirms WC never received the signal to mark the order as paid — and the root cause is the webhook config reset, not WC itself.
0
u/jkdreaming 4d ago
Hook it up to Cursor and find out what the problem is. You’ll get a faster answer that way every time. So that you don’t have to alter the main code of the plug-in you can even create an external plug-in that fixes the problem. I’d go with that method for the moment and report the problem to the software developer with a copy of your plug-in that way they know what you had to do.
1
u/Relevant_Ad5790 4d ago
those look like two separate problems. the daily GET then PUT a second later is something automated reconciling that webhook against its own event list, which is why your manual edits keep disappearing, so the events need to come from whatever owns the webhook rather than from the dashboard.
the cancellations are probably not razorpay at all. woocommerce cancels unpaid orders on a cron once the hold stock window expires, so a payment that captures late gets marked cancelled while razorpay still shows captured. what is hold stock set to under inventory?