A client got in touch about an order in their WooCommerce store. A customer had paid with POLi, the payment had gone through at their bank, and the order in WordPress said cancelled.
They’d been fixing these by hand for months. Someone pays, the order sits unpaid, and an hour later WooCommerce cancels it with the note “Unpaid order cancelled - time limit reached”.
Nothing was broken on the site. Checkout worked and money was arriving.
The message that tells WordPress the money arrived had been blocked by the site’s own firewall for 13 months.
What the client saw
A New Zealand sports membership organisation, selling event entries and memberships through WooCommerce, hosted on WP Engine. A good share of their customers pay with POLi rather than a card, which is common enough here.
From the outside the store looked healthy:
- Checkout completed and sent customers to the thank-you page
- The POLi flow took the customer to their internet banking and back
- Money landed in the client’s account
- Card orders through the other gateways completed normally
Only the POLi orders got stuck, and POLi handled a small slice of the volume, so the pattern took a long time to look like a pattern.
Why POLi confirmations fail quietly
POLi can’t tell your website anything at the moment of payment. The customer leaves your site, logs into their internet banking, authorises the transfer, and comes back. Your site has no idea what happened while they were away.
So POLi sends a separate message, machine to machine, from their servers straight to yours. They call it a nudge. It says “this transaction succeeded”. WooCommerce hears that, marks the order paid, sends the receipt, and everything looks the way it should.
That nudge is the entire payment confirmation. Miss it and the order stays exactly where the customer left it, pending, with no record that anything went wrong.
WooCommerce then does something reasonable that makes it worse. It holds stock for unpaid orders for 60 minutes by default, and when the hold expires it cancels the order to put the stock back. So roughly an hour after a successful payment, the order kills itself.
How we traced it
We worked backwards from the cancelled orders.
Had a nudge ever arrived? Yes. The POLi plugin writes a note on the order every time one lands. We pulled every POLi order ever placed and read the notes. The last one to arrive was in June the previous year. 13 months of silence, and a clean line in the data where it stopped.
Was POLi still sending them? They said yes, every time, and that they got a 403 Forbidden back on every attempt. They also said something that mattered more: they never retry. One nudge per transaction, and a failure is left alone forever. That explained why nothing ever recovered on its own.
Was WP Engine blocking it? Their first log search came back with no such requests at all, which cost us a few days, because it had been run on the wrong field. So we got POLi’s sending IP addresses and user agent string, and started testing the site ourselves. Everything we sent got through, until we stopped making our requests look like a person.
The cause
The site had rules in WP Engine’s access rules tool blocking traffic that doesn’t look like a real browser. Empty user agent strings, ancient browser versions. Sensible rules, added for good reason around the middle of the previous year.
Around the middle of the previous year is also when the nudges stopped arriving.
We got the nudge itself allowed through, and it worked. The order note appeared, second for second, matching the timestamps in POLi’s log. Except POLi’s log still recorded a failure.
The reason took another week to pin down, and it’s a good one. When a nudge lands, WooCommerce processes the payment and then redirects the caller to the order received page. POLi’s client follows that redirect, and it judges success on the final response.
We asked WP Engine to search their edge logs by source IP rather than by URL. That produced the answer in one go: every one of POLi’s redirect requests arrived with an empty user agent field.
Their client sends POLi-Nudge/1.0 on the nudge itself and sends no user agent at all when following the redirect. So the second request hit the blank-user-agent block and got a 403. The payment was already complete by then, but POLi saw the 403 and filed the whole transaction as failed.
Our own rule, blocking our own payment provider, on a URL nobody had thought to test.
What we did about it
- Added an allow rule for the order received path in WP Engine’s access rules, scoped to the prefix
/checkout/order-received/. New rules land at the top of the list, above the blank-user-agent deny, so it wins. Give it a couple of minutes to propagate before you test. - Kept the rest of the blocking intact. The checkout page itself still refuses blank user agents.
- Had POLi re-send a past nudge to prove it end to end. Their log came back 200. That’s the exact request that had been failing for 13 months, working, from their side.
- Audited every POLi order ever placed so the client knew where they stood. No money was missing. The stuck ones had all been completed by hand, and the one unsettled order was a customer who never picked a bank.
The part that matters more than the fix
The fix was one firewall rule. Finding it took three weeks, most of that spent proving to POLi and WP Engine that the problem was real.
What should worry you is the 13 months before that. Nothing was watching the gap between “a customer paid” and “the order says paid”. WooCommerce doesn’t check. POLi doesn’t check, it fires once and forgets. The host doesn’t check, because from where it sits a 403 is a rule working correctly.
The only alarm in the whole system was a person, and they only heard about the orders where the customer bothered to chase.
Firewall rules are the particular trap here. Somebody adds one to stop bots, and it does stop bots. It also refuses every automated system that talks to your site, and those systems don’t complain to you. They complain to their own log file, inside a company you don’t work at.
This is most of what a website care plan is actually for. Someone whose job is to notice the orders that stopped completing, before your customers have to.
The technical detail
The nudge is a server-to-server POST to a WooCommerce API endpoint, identified by the query string wc-api=poli_nudge. It carries no browser characteristics because no browser is involved. Any rule that filters on “does this look like a browser” will catch it.
There were three refusal layers in front of those URLs, each with its own signature. A plain nginx 403 came from the access rules on the install. A 403 carrying a cf-mitigated: challenge header came from WP Engine’s Global Edge Security, which is Cloudflare Enterprise underneath, and a server-to-server request can never solve a challenge because it can’t run JavaScript. A 200 meant it had passed both. The response headers told us which layer to go and argue with.
curl reproduces all of it. Send the same request with a browser user agent, with POLi-Nudge/1.0, and with -H "User-Agent:" to force it blank. The gap between those three responses is the whole diagnosis. We’d have got there faster by testing the redirect target as well.
The POLi plugin never stored transaction IDs. It called the setter and never saved the order, so every order had a null reference and we had nothing to quote back to POLi when asking them to look up a payment. If you take money through a smaller gateway plugin, check that reference lands in the database before you need it.
If your orders are cancelling themselves
- Open a cancelled order and read the order notes. “Unpaid order cancelled - time limit reached” means the payment confirmation never arrived. It doesn’t mean the customer didn’t pay.
- Find the most recent order where a confirmation did arrive. Every gateway leaves a note. The date it stops is the date to investigate.
- Line that date up against your change log. New security plugin, new firewall rule, host migration, CDN switched on.
- Ask your gateway what response they’re getting, and whether they retry. POLi will give you the status code and time for a given transaction. Plenty of gateways never retry, so every failed confirmation is lost rather than delayed.
- Test the confirmation URL with curl, including with a blank user agent, and test the page it redirects to as well.
- If you’re on WP Engine, read your access rules before you blame anyone else. Ours were the culprit.
If orders on your store are cancelling themselves and nobody can tell you why, that’s what our quick fix service is for. If you’d rather somebody spotted it before a customer does, have a look at our website care plans.