Skip to content
  • FG-53
  • 13 min read
  • Last checked November 7, 2026

How to Fix WooCommerce Orders Stuck in “Pending payment” or “On hold” and Order Emails Not Sending

Customers paid but orders sit in Pending payment or On hold, and no order emails arrive. Prove whether the gateway webhook reaches your site, then fix delivery and email.

Run the health check

The customer says they paid. The money shows in Stripe or PayPal. But in WooCommerce the order sits at Pending payment, or at On hold, and neither you nor the customer received an order email. An hour later the order may even flip to Canceled and the stock goes back on sale.

Under the hood, the payment and the order are two separate records on two separate systems. The gateway has to tell your store that the payment succeeded, usually by sending a server-to-server message called a webhook (PayPal’s older name is IPN). If that message never arrives, or arrives and is rejected, the order never moves forward, and because order emails are triggered by status changes, no emails go out either. This guide is for store owners and the people who support them.

What is happening

WooCommerce order statuses each mean something specific. Knowing which one you are stuck in narrows the cause straight away.

Status What it means Common reason it is stuck
Pending payment Order created, no payment confirmed yet The shopper abandoned the payment page, or the gateway’s confirmation never reached the site
Failed A payment attempt was declined or errored Card declined, 3D Secure not completed
On hold Awaiting manual action Bank transfer or check by design; a card authorized but not captured; a gateway flagging an amount or currency mismatch or a fraud review
Processing Payment received, ready to fulfill This is where a paid order should land
Completed Fulfilled Set by you, or automatically for virtual downloadable orders
Canceled Canceled by you, or by WooCommerce when unpaid for too long The hold stock timer expired on a pending order

Two settings matter here. Hold stock (minutes) in WooCommerce > Settings > Products > Inventory defaults to 60. When stock management is on and an unpaid checkout order passes that age, WooCommerce cancels it. And the New order email to you, like the Processing order email to the customer, fires on a status change such as Pending payment to Processing. An order that stays pending sends nothing at all, which is why stuck orders and missing emails usually arrive together.

A typical webhook flow: the shopper pays on the gateway’s page or in its embedded form; the gateway sends a signed POST to your site; the gateway plugin verifies the signature, finds the order and calls payment_complete(); the order moves to Processing and emails go out. Each gateway listens at its own address:

Gateway plugin Address the gateway posts to
WooCommerce Stripe Payment Gateway https://example.com/?wc-api=wc_stripe
WooCommerce PayPal Payments https://example.com/wp-json/paypal/v1/incoming
Legacy PayPal Standard (IPN) https://example.com/?wc-api=WC_Gateway_Paypal
Other gateways Shown in the plugin’s settings or documentation

Before you start

  • Access: a WordPress administrator account, the payment gateway dashboard, and SSH or a file manager if you can get it.
  • Do not refund or recreate orders yet. First confirm in the gateway dashboard which orders were really paid. Marking an unpaid order as Processing ships goods for free.
  • Backup: the checks change nothing. Back up the database before bulk-editing orders or clearing scheduled actions.
  • Running commands on the server: SSH in and cd to the folder containing wp-config.php. WP-CLI is wp on most hosts and wp-cli on some.
  • Running commands on your computer: curl ships with macOS, Linux and current Windows (use curl.exe in PowerShell).

Step 1: Read the order notes and the gateway log

Open a stuck order. The order notes in the sidebar are a timeline written by WooCommerce and the gateway plugin. Read them from the oldest up.

  • No gateway note at all: the shopper may never have completed payment. Check the gateway dashboard for a matching payment before going further.
  • A note that payment was started, then nothing: the confirmation webhook did not arrive or was rejected. Continue to Step 2.
  • “Stripe charge authorized … Process order to take payment, or cancel to remove the pre-authorization”: the store is set to authorize only. On hold is correct; the money is reserved, not taken. Change the order to Processing to capture, or change the gateway’s capture setting if you want immediate charges.
  • A note about a mismatch (amount, currency or email): the gateway refused to complete the order automatically. Compare the figures and complete it manually if the payment is genuine.

Next, open WooCommerce > Status > Logs and pick the gateway’s source, for example woocommerce-gateway-stripe once Log debug messages is checked in its settings. Stripe logs every rejected webhook with a line that starts Webhook validation failed followed by the reason, and logs Webhook ignored: the event's Stripe account does not match the connected account when events come from a different Stripe account than the one the store is connected to. That second one happens after a store switches Stripe accounts while an old webhook endpoint keeps firing.

Step 2: Prove whether the webhook reaches your site

Check delivery from the gateway dashboard

This is the most reliable test, because it shows what the gateway itself saw.

  • Stripe: find Webhooks in the developer area of the Stripe Dashboard (Stripe has been moving it into a section called Workbench, and menu names change, so search the Dashboard for “webhooks” if you cannot see it), choose the endpoint for your store, and open its list of event deliveries. Each attempt shows the HTTP status your server returned. You can open a failed event and resend it once the cause is fixed. Make sure you are looking at the same mode (test or live) as the store.
  • PayPal: in the PayPal developer dashboard, open the app your store uses and look for its webhook events, which show the delivery status of each event. PayPal rearranges these screens from time to time, so the exact labels may differ. The PayPal Payments plugin registers its webhook for you and includes tools in its settings to resubscribe webhooks and simulate one.
  • Other gateways: look for a webhook, notification or IPN history page in the gateway account.
Response the gateway recorded Cause Fix
No attempts listed No endpoint configured for this mode, or events not selected Use the plugin’s connect or webhook setup button, or add the endpoint with the address from the table above
301 or 302 Endpoint uses http, the wrong www form, or an old domain Change the endpoint to the exact final URL; webhook senders treat redirects as failures
403 Firewall, security plugin, Cloudflare challenge, or REST API blocked Step 3
404 Wrong path, plugin inactive, or permalinks broken for REST routes Check the plugin is active and the URL is exact
401 Password protection on the site (staging basic auth) Exclude the webhook path from the password, or test on a reachable copy
500 PHP error while processing the event Read fatal-errors on the Logs page
503 Maintenance or coming soon mode, or server overload Exclude the path or switch maintenance off; see FG-14 for resource limits
Timed out The site took too long to answer Server performance; see FG-13
2xx but orders still pending The plugin received the event but rejected it (signature or account mismatch) Read the gateway log from Step 1

Test the endpoint yourself

You can check that a webhook address reaches WordPress at all with one request from your computer. This example uses the Stripe address:

curl -i -X POST "https://example.com/?wc-api=wc_stripe" -H "Content-Type: application/json" -d "{}"

Placeholder: replace example.com with your live store domain, exactly as it appears in the browser. For another gateway, swap in its address. Flags: -i prints the response headers so you can see the status and server, -X POST sends a POST as the gateway does, -H sets the content type, and -d "{}" sends an empty JSON body. Good output: a 2xx status from your own server. The current Stripe plugin answers an unsigned request with 204 and writes a Webhook validation failed line to its log, which proves the request reached the plugin. Its webhook status in the settings will also show your test as a failed delivery, so expect that. Bad output: 403 with a firewall or Cloudflare page, 301 with a location header pointing somewhere else, or 401. Testing from your computer does not prove the gateway’s own servers are allowed, because some firewalls treat traffic differently by country or reputation, so always confirm with the dashboard.

The signing secret and the clock

Gateways sign each webhook. Stripe’s secret starts with whsec_ and differs per endpoint and per mode. If you recreated the endpoint, or connected a different account, the store’s saved secret no longer matches. The plugin’s connect flow normally refreshes it. Stripe’s plugin also rejects events whose timestamp is more than five minutes away from your server clock; check with date -u over SSH.

Step 3: Let the webhook through your firewall

A 403 is the most common delivery failure on stores that added security after launch. Find the blocked request first, then allow only what is needed.

  • Cloudflare: open Security Events, filter by the path (wc-api or /wp-json/paypal/) and the time of a failed delivery. A managed challenge or Bot Fight Mode cannot be solved by a server. Create a custom rule that skips the relevant protections for that exact path and the POST method only. Cloudflare documents that Bot Fight Mode cannot be skipped by custom rules, so if it blocks webhooks you must turn it off or use Super Bot Fight Mode, which does support skip rules.
  • Security plugins: check the plugin’s live traffic or block log. Settings that disable the REST API for logged-out users break REST-based webhooks such as PayPal Payments. Allow the specific route rather than turning protection off.
  • ModSecurity or a host firewall: the block shows in the server error log or the hosting panel. Ask your host to whitelist the rule ID for that path.
  • Country blocking: gateways send webhooks from their own data centers, which may not be in the countries you allow.

Wider 403 diagnosis is covered in FG-49. Once fixed, resend a failed event from the gateway dashboard and watch the order change.

Step 4: Check Action Scheduler and WP-Cron

Several order jobs run in the background through Action Scheduler, WooCommerce’s job queue, which is triggered by WP-Cron. These include canceling unpaid orders when the hold stock timer expires (woocommerce_cancel_unpaid_orders), delivering WooCommerce’s own outgoing webhooks, and, if you enabled the Deferred emails feature in WooCommerce > Settings > Advanced > Features, sending every transactional email (woocommerce_send_queued_transactional_email). Some gateways also queue follow-up work there. If the queue is stalled, emails can be stuck behind it.

Open WooCommerce > Status > Scheduled Actions and filter by Pending. Many actions whose scheduled date is in the past mean the queue is not running. Or check from the server:

wp action-scheduler action list --status=pending --per_page=20 --fields=id,hook,status,scheduled_date
wp action-scheduler action list --status=failed --per_page=20 --fields=id,hook,scheduled_date
wp cron event list --fields=hook,next_run_relative | head -n 15

Where: server SSH, site folder. Flags: --status chooses pending or failed actions, --per_page=20 limits the list, and --fields picks the columns. wp cron event list shows WordPress cron events and next_run_relative how far away each is. No placeholders. Good output: pending actions scheduled in the future, few or no failures, and cron events due in the near future. Bad output: pending actions dated hours or days ago, or cron events showing now for a long time. That means WP-Cron is not firing, often because DISABLE_WP_CRON is set in wp-config.php with no real cron job to replace it, or because the site gets no traffic.

To work through the backlog now, run the queue for one hook:

wp action-scheduler run --hooks=HOOK_NAME --batch-size=25

Placeholder: HOOK_NAME is a hook from the list above, such as woocommerce_send_queued_transactional_email. Flags: --hooks limits the run to that hook (comma separate several) and --batch-size sets how many actions run per batch. Good output: a success line saying how many actions were processed. Bad output: errors naming a plugin, which you then read in the logs. For a lasting fix, set a server cron job to call WP-Cron every few minutes. Action Scheduler’s effect on admin speed is covered in FG-33.

Step 5: Fix order emails

Is the email enabled and addressed correctly?

Open WooCommerce > Settings > Emails. Click New order and check that Enable/Disable is checked and Recipient(s) holds the right addresses, separated by commas. Check Processing order and Completed order too. Then check “From” address under the email sender options: it should be an address on your own domain, or receiving servers will reject or junk the message.

Did WooCommerce try to send it?

Since WooCommerce 10.9, every send attempt is logged under the transactional-emails source on the Logs page, and a private note is added to the order such as Email "New order" sent. or Email "Processing order" failed to send: followed by the reason from wp_mail. On older versions, install an email log plugin to get the same evidence.

  • No attempt logged: the trigger never fired. The order did not change status (Steps 1 to 3), or the email is disabled.
  • Failed to send: wp_mail itself returned an error, often Could not instantiate mail function or an SMTP authentication message. The server cannot hand mail off.
  • Sent, but nobody received it: WordPress handed the message to the mail system and it was lost, rejected or filtered later.

The last two are mail delivery problems, not WooCommerce problems. Setting up authenticated SMTP or an email API, and reading the mail log, is covered step by step in FG-42.

Resend the emails

Once the cause is fixed, open each affected order. In the Order actions box choose Resend new order notification to send your copy again, or Send order details to customer to email the customer their order details, then click the update button. WooCommerce records Order details manually sent to customer. in the notes. If a paid order is still pending, changing its status to Processing yourself also triggers the normal status emails.

Advanced: Find every paid order the store missed

After an outage you need a list of orders that are paid at the gateway but still pending in the store. Pull the pending orders with their gateway transaction IDs:

wp wc shop_order list --user=ADMIN_USERNAME --status=pending --after=YYYY-MM-DDT00:00:00 --fields=id,date_created,total,payment_method,transaction_id --format=csv > pending-orders.csv

Where: server SSH, site folder. Placeholders: ADMIN_USERNAME is the login of an administrator (WooCommerce’s CLI runs through the REST API and needs a user with permission); YYYY-MM-DD is the day the problem started. Flags: --status=pending picks only pending orders, --after limits by creation date, --fields chooses the columns, --format=csv makes a file you can open in a spreadsheet, and > pending-orders.csv saves it in the current folder. Good output: a CSV with one row per order. An empty transaction_id is normal for orders where the gateway never confirmed anything. Bad output: an error saying you are not allowed to list resources, which means the user is not an administrator.

Export payments for the same period from the gateway dashboard and match them by order number (most gateways store the WooCommerce order number in the payment description or metadata). For each match, resend the event from the gateway dashboard if it is still available, which lets the plugin record the transaction ID properly. Otherwise set the order to Processing and add a note with the gateway’s payment ID. Delete the CSV from the server when you are finished, because it contains order data.

For stores where unpaid orders are routinely canceled while customers are still on slow bank authentication screens, consider raising Hold stock (minutes). Leaving it blank disables automatic cancellation entirely, which keeps abandoned orders pending forever, so choose a value rather than clearing it.

When to contact your host

Contact your host or open a ticket when deliveries show 403, 503 or timeouts that you cannot match to a plugin or Cloudflare rule, when the server clock is wrong, when WP-Cron cannot be replaced with a real cron job from your control panel, or when wp_mail fails at the server level. Send:

  • The webhook URL and the exact UTC times of failed deliveries, with the status code the gateway recorded.
  • The gateway’s delivery details (response body if shown) and any Cloudflare Ray ID.
  • Relevant lines from the gateway log, fatal-errors or transactional-emails, with customer details removed.
  • What changed: domain move, new firewall, new plugin, or a gateway account change.

The diagnostic tools page can help you test the endpoint and headers. If orders are failing right now and you are losing sales, start at the Emergency Room.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close