How to Fix “No shipping options were found” in WooCommerce
The cart or checkout says no shipping options were found for an address. Turn on shipping debug mode to see which zone matched, then fix the zone order, ZIP codes, methods or carrier plugin.
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.
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.
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 |
cd to the folder containing wp-config.php. WP-CLI is wp on most hosts and wp-cli on some.curl ships with macOS, Linux and current Windows (use curl.exe in PowerShell).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.
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.
This is the most reliable test, because it shows what the gateway itself saw.
| 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 |
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.
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.
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.
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.Wider 403 diagnosis is covered in FG-49. Once fixed, resend a failed event from the gateway dashboard and watch the order change.
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.
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.
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.
wp_mail itself returned an error, often Could not instantiate mail function or an SMTP authentication message. The server cannot hand mail off.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.
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.
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.
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:
fatal-errors or transactional-emails, with customer details removed.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.
Did this fix it?
Still stuck? Start again from what you can see in 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 checklistsKeep reading
Search the field manual
Or jump straight to a symptom
Press / to search from any page, Esc to close