Notes
The pay for order page: a second way to take a card
WooCommerce ships a second payment surface for orders that already exist. It calls your gateway directly, and none of your checkout validation runs there.
6 min read
Most stores protect the checkout form and stop there, which is reasonable, because that is where orders come from. It is also where a second route hides in plain sight. The WooCommerce pay for order page takes a card for an order that already exists. WooCommerce enables it by default, and almost nobody tests it.
It is worth understanding for one blunt reason. The pay for order page runs your gateway without running your checkout validation.
What the WooCommerce pay for order page is for
Open any order in the admin and there is a link near the top of it. WooCommerce documents it as the "Customer payment page", and says "You can share this link with the customer for them to follow and complete payment". The WooCommerce pay for order page exists for perfectly ordinary work. A phone order somebody wants to pay for later. A manual invoice. A failed payment you would like the customer to retry without rebuilding their basket.
Technically the WooCommerce pay for order page is an endpoint rather than a page. WooCommerce's developer documentation describes endpoints as "an extra part in the website URL that is detected to show different content when present". The pay endpoint takes the form /order-pay/{ORDER_ID}, appended to your checkout page URL. You will find its slug under WooCommerce, Settings, Advanced, alongside order received and add payment method. It is not an add-on and it is not optional in any meaningful sense: renewals, invoices and retry links all rely on it.
What the gate actually checks
The handler is short enough to read in full, and reading it is the fastest way to understand the difference between this route and the checkout. It runs on a submitted pay form and verifies the form's own nonce. Then it makes exactly three checks. The order ID in the URL must match the order it loaded. The key in the query string must match the order's key, compared in constant time. The order must still need payment. If all three hold, it sets the payment method and calls the gateway.
Notice what it tests. Not who you are. The order key in the URL is the credential, which is by design, because guests do not have accounts and still need a way back to their order.
WordPress does add identity checks on top of that, and the documentation spells them out rather than leaving them to folklore. If the order belongs to a registered customer who is logged out, "the customer will be prompted to log in to their account to continue to the payment form". If the order belongs to a guest, "anyone who follows a payment link more than 10 minutes after the order was created will be prompted to verify the email address on the order". And if there is no email address on a guest order at all, "anyone with the payment link will be able to pay for the order". Ten minutes is a grace period for the person who just placed the order, not an oversight. It is still a window with a defined width, and worth knowing about.
Why your checkout defences may not be here
The classic checkout runs an ordered sequence of hooks as it processes a submission, including woocommerce_checkout_process and woocommerce_after_checkout_validation. Nearly every anti-abuse integration for WooCommerce hangs off one of those. That is where it can inspect a submission and refuse it before an order exists.
The WooCommerce pay for order page does not run that sequence. There is no cart to validate, no order to create, and no checkout validation to run. It fires its own pair of actions before and after the payment. In between it sets the payment method, calls the gateway's field validation, and then calls the gateway to take the money. A check that only listens for checkout validation is not asked for an opinion at any point in that flow.
Script loading is subtler, and worth testing rather than assuming. The endpoint sits on the checkout page, so is_checkout() returns true there. An integration that picks where to load its JavaScript by asking whether this is the checkout will usually load it. Whether anything then reads what that JavaScript produced is a separate question, and the answer is often no.
The pattern this creates
Put the pieces together and a shape emerges that explains order lists which otherwise look strange.
Something creates an order first. The classic form, the block checkout, or the draft that the block checkout opens as soon as someone starts filling in the form. It does not have to have come from a page you know about, because a store can carry more than one page that takes orders. The WooCommerce pay for order page then accepts submission after submission against that single order for as long as it still needs payment. Each submission is a fresh call to your gateway with fresh card details, and none of them adds a row to your order list, because the order already exists.
This is the same lesson as reading failed orders, arriving from a different direction: your order count is not your attempt count. Compare the authorisation count on your processor's dashboard against the order count in WooCommerce. If the first is far higher, the gap has to be somewhere, and a retry surface that accepts unlimited attempts against one order is a strong candidate.
What to check on your own store
- Open an order that needs payment, follow the customer payment page link, and see whether whatever protects your checkout does anything at all here. The Checkout Scanner reports the routes it finds, which is the same question asked automatically. If you cannot tell, you do not have logging on that surface.
- Compare authorisation counts at your payment provider with order counts in WooCommerce over the same period. A large gap needs an explanation.
- Read the order notes on individual orders rather than the list. An order carrying many gateway decline notes shows a retry surface in use, whatever the status column says.
- Check whether your store leaves guest orders without an email address, since those payment links stay open to anyone holding them.
- Decide what should happen after a small number of failed attempts on one order. WooCommerce will keep accepting them, so the limit has to come from somewhere else.
What not to do is remove the endpoint. Renewals, invoices and legitimate retries depend on it. A store that breaks the link in its own emails has traded one problem for a worse one.
Where Checkout Bouncer fits
Checkout Bouncer is a free, GPL plugin on WordPress.org built around one observation. A WooCommerce store has several routes that can take a payment. A captcha on one of them protects one of them. It is a free, GPL plugin on WordPress.org, and it scores four surfaces with Google reCAPTCHA v3: the classic checkout, the Checkout block, the WooCommerce pay for order page and add payment method. It maps the routes an order can take into a store, rather than assuming there is only one. It is checkout only, not login or registration, and it is neither a firewall nor a malware scanner. Pro remains a plan rather than a product, so there is nothing to buy. The checks above are worth running whether or not you install anything.