Skip to content
  • FG-52
  • 18 min read
  • Last checked November 8, 2026

How to Fix WooCommerce Checkout Not Working: “There was an error processing your order” and the Endless Spinner

Place order spins forever or shows “There was an error processing your order”. Read the checkout request in the Network tab, check the logs, then fix the gateway, cache or script.

Run the health check

A shopper fills in the checkout form, clicks Place order, and then one of three things happens. The button spins and never stops. A red box appears saying “There was an error processing your order. Please check for any charges in your payment method and review your order history before placing the order again.” Or a payment gateway message appears, such as an invalid API key or a card that cannot be charged. Either way, no money is taken.

The generic message is WooCommerce telling you that the browser asked the server to process the order and got back something it could not understand: an HTTP error, a page of HTML instead of JSON, or a JSON reply with no explanation. The real cause is almost always visible in one network request and one log file. This guide is for store owners and the designers or developers who look after their stores.

What is happening

On the classic checkout (the [woocommerce_checkout] shortcode), WooCommerce’s checkout.js collects the form and sends it as a POST request to https://example.com/?wc-ajax=checkout. On the server, WooCommerce checks the security nonce, checks the cart is not empty, validates the fields, creates the order with the status Pending payment, and calls the payment gateway. The gateway talks to its own API (Stripe, PayPal and so on) and returns either success with a redirect URL, or failure with a message. WooCommerce sends that back as JSON, and the script either redirects the shopper to the thank you page or prints the error.

The block checkout does the same job through the Store API instead: the browser sends a POST to /wp-json/wc/store/v1/checkout with a Nonce header, and the order exists as a hidden checkout-draft until payment is attempted.

In checkout.js, the generic message appears when the request fails at the HTTP level or the JSON has no messages in it. It is a symptom, not a diagnosis.

What the shopper sees What it usually means
Spinner never stops The request is still waiting (slow gateway call, PHP timeout) or a JavaScript error stopped the script before it could show a result.
“There was an error processing your order” The server returned an HTTP error, HTML, a PHP warning mixed into the JSON, or an empty failure.
“We were unable to process your order, please try again.” The checkout nonce failed. Usually a cached checkout page carrying someone else’s nonce.
“Sorry, your session has expired.” The nonce failed and the cart is empty on the server. Session and cookie problems are covered in FG-54.
A gateway message (invalid key, declined, authentication) The gateway API rejected the call. This is configuration or the card, not WooCommerce.

Before you start

  • Access: a WordPress administrator login, your payment gateway dashboard, and ideally SSH or a file manager on the hosting account.
  • A safe way to test: put the gateway in test mode on a staging copy if you have one, or use a low-value real product you can refund. Never leave a live store in test mode, because real customers will be told their order went through when no money moved.
  • Backup: the diagnostic steps change nothing. Take a database backup before switching plugins off or changing checkout pages.
  • Running commands on the server: connect over SSH and cd into the site folder, the one that contains wp-config.php. WP-CLI is called wp on most hosts and wp-cli on some container platforms. Use whichever exists.
  • Running commands on your computer: curl is built into macOS, Linux and current Windows (type curl.exe in PowerShell so the alias does not interfere). WSL also works.
  • Browser tools: press F12 (Cmd+Option+I on a Mac) for DevTools.

Step 1: Read the checkout request in the Network tab

  1. In a private window, add a product to the cart and fill in the checkout form.
  2. Open DevTools, choose the Network tab and check Preserve log so the record survives a redirect.
  3. Type checkout in the filter box.
  4. Click Place order.
  5. Click the request named ?wc-ajax=checkout (classic) or checkout under wc/store/v1 (block). Read the Status on the Headers tab, then open the Response tab.
Status and response Meaning Go to
200, JSON with "result":"success" and a redirect, but no redirect happened A script on the page broke the redirect handling Step 5
200, JSON with "result":"failure" and messages Validation or gateway refusal; the message is the answer Step 3
200, but the response starts with HTML, Warning:, Notice: or Deprecated: before the { PHP is printing errors into the response Step 2
500 PHP fatal error during checkout Step 2
403 A firewall, ModSecurity rule or Cloudflare WAF blocked the POST Step 4
301, 302 or 404 The request was redirected, often by a www or https rule, or a security plugin hiding wc-ajax Step 4
(pending) for a long time, then 502, 504 or 524 PHP is stuck waiting, usually on the gateway API or a slow plugin hook Step 3 and FG-13
No request at all JavaScript failed before sending Step 5

Also look at the Console tab. The classic checkout script logs Unable to fix malformed JSON when it found junk around the JSON that it could not strip. That line alone proves something on the server is printing output into the response.

To check whether the wc-ajax endpoint is reachable at all, run this on your computer:

curl -s -o /dev/null -w "%{http_code}\n" -X POST "https://example.com/?wc-ajax=get_refreshed_fragments"

Placeholder: replace example.com with your store’s domain exactly as it appears in the address bar (with or without www). Flags: -s hides progress output, -o /dev/null discards the body (use NUL on Windows), -w "%{http_code}\n" prints only the status code, and -X POST sends a POST as the real checkout does. Good output: 200. Bad output: 403 means something blocks POSTs to wc-ajax; 301 means your domain or protocol does not match what you typed, which you should fix before anything else.

Step 2: Read the WooCommerce logs and the PHP error log

WooCommerce > Status > Logs

WooCommerce records uncaught PHP fatal errors under the source fatal-errors. Recent versions also keep a step by step trace of each place order attempt under sources named place-order-debug- followed by a short ID. Logs for orders that completed are deleted in the background, so the ones that remain are usually the attempts that failed. Gateways write their own sources too; for Stripe it is woocommerce-gateway-stripe once you check Log debug messages in its settings.

By default these logs are files in wp-content/uploads/wc-logs/. The settings screen on the same Logs page shows the storage location, the retention period in days and the level threshold. On the server you can list and read them directly:

ls -lt wp-content/uploads/wc-logs/ | head -n 15
tail -n 40 wp-content/uploads/wc-logs/fatal-errors-YYYY-MM-DD-HASH.log

Where: server SSH, in the site folder. Placeholders: YYYY-MM-DD is the date in the file name and HASH is the random string WooCommerce appends; copy the full name from the ls output. Flags: -lt lists newest first with sizes and times, head -n 15 keeps only the top 15 lines, tail -n 40 shows the last 40 lines of the file. Good output: no fatal-errors file for today. Bad output: a line such as CRITICAL Uncaught Error: Call to undefined method ... in /wp-content/plugins/PLUGIN_SLUG/.... The path tells you which plugin or theme crashed. If the folder has moved, the Logs settings page names the real location.

PHP warnings polluting the JSON

If the response starts with Warning: or Deprecated:, PHP is configured to display errors on screen. The checkout itself may have worked, but the browser cannot parse the reply. On a live store, errors should go to a log, not the screen. In wp-config.php (a file you edit), make sure these lines sit above the line that says to stop editing:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

What each does: WP_DEBUG turns on error reporting, WP_DEBUG_LOG writes it to wp-content/debug.log, and WP_DEBUG_DISPLAY plus display_errors stop it being printed into pages and AJAX replies. There are no placeholders. Good result: the checkout response becomes clean JSON and the warnings appear in debug.log instead. Turn WP_DEBUG back to false when you are done. General fatal error handling and recovery mode are covered in FG-02.

Step 3: Check the payment gateway configuration

Test mode versus live mode

Every major gateway has two separate sets of credentials. Stripe keys start with pk_test_ and sk_test_ in test mode and pk_live_ and sk_live_ in live mode. Webhook signing secrets start with whsec_ and are different for each endpoint and each mode. PayPal has sandbox and live apps with different client IDs. Mixing them produces errors such as Stripe’s Invalid API Key provided or payments that succeed in one dashboard while the store never hears about them.

To confirm: open WooCommerce > Settings > Payments, then the gateway, and note whether test mode is checked. Check the gateway dashboard is in the same mode. If the plugin offers a connect or reconnect button, prefer it to pasting keys, because it fetches a matching set.

Read the gateway’s own error

Turn on the gateway’s debug logging, place one test order, and read its log source. The gateway dashboard also keeps a request log of API calls and responses. A 401 there means bad keys. A 402 or a decline code means the card was refused, which is the bank’s decision and not a fault in your store.

Server clock and outbound connections

Your server must reach the gateway API over HTTPS. If the checkout hangs and the log shows cURL error 28 (timed out) or cURL error 6 (could not resolve host), the hosting server cannot reach the gateway; that is a ticket for your host. A server clock that has drifted can also break signed requests. The Stripe plugin, for example, rejects webhooks whose timestamp is more than five minutes away from the server’s time. Check with date -u over SSH and compare it with the real UTC time.

Webhooks, which tell your store a payment completed after the shopper has left, are the subject of FG-53. If orders are created but stay in Pending payment, go there.

Step 4: Rule out caching, redirects and firewalls

Is the checkout page cached?

WooCommerce sends no-cache headers on the cart, checkout and my-account pages and defines DONOTCACHEPAGE so well-behaved cache plugins skip them. A server cache or CDN rule that ignores those signals will serve one shopper’s checkout HTML to the next, complete with a nonce that belongs to a different session. The result is “We were unable to process your order, please try again.” on the first click. Check the headers from your computer:

curl -sI "https://example.com/checkout/" | grep -iE "cache-control|x-litespeed-cache|cf-cache-status|x-cache|age"

Placeholder: use the real address of your checkout page; WooCommerce > Settings > Advanced shows which page is assigned. Flags: -s silences progress, -I requests headers only, and grep -iE filters case-insensitively for the cache headers. Good output: a cache-control line containing no-cache and max-age=0, and any cache status showing MISS, BYPASS or DYNAMIC. Bad output: HIT, or an age header with a number above zero. Run it twice, since the first request can be a miss that primes the cache. The exact cache exclusions for LiteSpeed, Nginx and Cloudflare are in FG-54.

403 and redirect responses

A 403 on ?wc-ajax=checkout nearly always comes from a web application firewall. Apostrophes in names or text in order notes can trip a ModSecurity rule. Check the security plugin’s block log and Cloudflare Security Events for the test time, and allow the specific rule rather than disabling the firewall. FG-49 covers 403 diagnosis in depth.

A 301 or 302 means the POST was redirected to another host and its body was lost. Make sure WordPress Address and Site Address in Settings > General match the canonical domain; FG-39 covers that setup.

Step 5: Check JavaScript optimization and Content Security Policy

If no checkout request is sent, or the gateway’s card field never appears, look at the Console tab. Two families of error matter.

Script errors from optimization. Delay JavaScript, combine, defer and minify features can load jquery, wc-checkout or the gateway’s scripts in the wrong order or only after the first scroll or click. Errors such as jQuery is not defined or wc_checkout_params is not defined are the giveaway. Exclude the checkout and cart pages from all JavaScript optimization, or exclude the gateway domains and WooCommerce’s checkout scripts. Cloudflare Rocket Loader causes the same pattern and should be off for checkout. The general method for finding which optimization broke a script is in FG-47.

Content Security Policy blocks. A message such as Refused to load the script 'https://js.stripe.com/v3/' because it violates the following Content Security Policy directive means a security header plugin or server rule does not allow the gateway. Stripe needs at least https://js.stripe.com in script-src and frame-src, https://hooks.stripe.com in frame-src for 3D Secure, and https://api.stripe.com in connect-src. Copy each gateway’s current list from its own documentation, because the lists change.

To confirm, switch the optimization feature off on staging (or for the checkout page only) and test again.

Step 6: Know which checkout you run: block or shortcode

Edit the page assigned as Checkout. If it contains the Checkout block, you run the block checkout. If it contains the shortcode [woocommerce_checkout] (or a Classic Checkout block), you run the classic one. New stores get the block checkout by default.

Feature Classic (shortcode) Block
Request on Place order POST /?wc-ajax=checkout POST /wp-json/wc/store/v1/checkout
Security token woocommerce-process-checkout-nonce form field Nonce request header (error woocommerce_rest_invalid_nonce)
Field customization PHP hooks such as woocommerce_checkout_fields and template overrides Those hooks do not change the block form; it uses its own extensibility API
Gateway support Any gateway The gateway must register block support; in the editor an unsupported one is marked “Incompatible with block-based checkout”
Blocked by Firewalls filtering wc-ajax Security plugins that disable or restrict the REST API

Two traps follow. A field editor written for the classic checkout does nothing on the block checkout. A security plugin that blocks /wp-json/ for logged-out visitors breaks the block checkout while the classic one keeps working. If a gateway or plugin you depend on is not block compatible, switching the page back to the classic checkout is a legitimate fix. The Checkout block offers a switch to the classic checkout in current versions; if your version does not, replace the block with a Shortcode block containing [woocommerce_checkout].

Step 7: Find a plugin or theme conflict

If the logs show nothing clear, isolate the conflict. On staging, switch to a default theme such as Storefront or Twenty Twenty-Five and deactivate everything except WooCommerce and the gateway. On a live store, the Health Check & Troubleshooting plugin’s troubleshooting mode does the same thing only for your own logged-in session, so customers are not affected.

wp plugin list --status=active --fields=name,version,update_version

Where: server SSH, site folder. Flags: --status=active limits the list to active plugins, and --fields picks the columns. No placeholders. Good output: a table to save as a record before you change anything. Plugins that touch checkout (field editors, fraud screening, address autocomplete, currency switchers) are the first suspects. Re-enable them one at a time with a test order after each.

Payment method missing at checkout

Sometimes the checkout works but a payment option simply is not there: the PayPal buttons do not appear, the Stripe card field is missing, or Apple Pay and Google Pay never show. Gateways decide for themselves whether to offer a method on each page load, and they hide it quietly when a condition fails. Work through these checks in order.

Is the method enabled and available for this cart?

  • In WooCommerce > Settings > Payments, confirm the method is switched on. Some gateways show a toggle as on while their own settings page still needs setup; turning the method off and on again and saving is a quick check.
  • If the order total is zero (a 100% coupon or a free product), WooCommerce asks for no payment and shows no payment methods. That is expected.
  • Some methods have their own restrictions, such as Cash on delivery limited to certain shipping methods. Read each method’s settings for conditions.
  • If you run the block checkout, the method must support it. In the editor, an unsupported gateway is marked as incompatible with block-based checkout (see Step 6).

Onboarding and connection status

Both PayPal and Stripe plugins connect your store to an account through an onboarding flow. If that flow was not finished, or was finished in the wrong mode, the buttons do not load.

  • PayPal Payments. Open WooCommerce > Status. The System Status report includes a WooCommerce PayPal Payments section that shows whether the account is onboarded and whether your store currency and country are supported. Also check the plugin’s setting for where smart buttons appear, since checkout can be left out.
  • Stripe and WooPayments. The settings page shows whether the account is connected and in test or live mode. In Stripe, live payments also need an activated account; an account still waiting on business details may not accept live charges.

Sandbox and live credentials after a move

A very common case: the store was built on a staging domain with sandbox or test credentials, then moved to the live domain. The gateway is still connected in sandbox mode, connected to the staging site’s account, or has keys from one mode and a setting from the other. Switch the gateway to live mode and reconnect through its connect button rather than pasting keys by hand, then place a low-value real order and refund it. Test mode versus live mode is covered in Step 3.

Currency, country and amount limits

  • Currency. PayPal supports a specific list of currencies, and many local payment methods only work in one currency. If your store currency (WooCommerce > Settings > General) is not supported, the method is hidden. Multi-currency plugins can switch the cart into an unsupported currency for some visitors only.
  • Country. Some features, such as PayPal Pay Later messaging, PayPal advanced card fields and several Stripe payment methods, are only offered to merchants or shoppers in certain countries. The billing country the shopper picks can change which methods appear.
  • Minimum and maximum amounts. Card processors have a minimum charge (Stripe’s documentation lists the minimum per currency, for example 0.50 US dollars), and buy now, pay later methods only appear between a minimum and maximum basket total. A very cheap test product can make these methods disappear.

Apple Pay, Google Pay and Link buttons

  • These express buttons only appear on devices and browsers that support them with a card saved in the wallet: Apple Pay in Safari on Apple devices, Google Pay where a card is saved to the Google account. Test on the right device before assuming they are broken.
  • Stripe requires each domain that shows these buttons to be registered as a payment method domain in the Stripe Dashboard. The WooCommerce Stripe plugin and WooPayments normally register the domain for you, but after a domain change or when moving from www to non-www, check it is listed for the domain shoppers actually use.
  • The page must be served over HTTPS.

Scripts blocked in the browser

PayPal’s buttons load from www.paypal.com/sdk/js and Stripe’s fields from js.stripe.com. If those scripts are delayed, deferred, combined, blocked by a Content Security Policy, held back by a cookie consent banner, or blocked by the shopper’s ad blocker, the method appears empty or not at all. Open the Console tab on the checkout and look for Refused to load the script or errors that mention paypal or Stripe. Step 5 above covers optimization and Content Security Policy fixes, and the cookie consent guide covers consent tools that block payment scripts. Ask the customer to try with browser extensions turned off if only some people report the problem.

If none of these explain it, test with a default theme and only WooCommerce and the gateway active (Step 7), then send the gateway’s support team your System Status report and the Console errors.

Advanced: Trace one failed order end to end

When a problem is intermittent, collect four views of the same attempt and line them up by time.

  1. The browser. In the Network tab, right-click the checkout request and choose Copy as cURL or Save all as HAR. Note the exact time in UTC.
  2. The WooCommerce trace. Open the newest place-order-debug- log. Each line is one step the order passed through, with context such as the order ID and, where PHP reported one, the last error. The last step written is where the attempt died. If an order ID was assigned, the order stores the log source in its meta so you can match them.
  3. The server access log. Find the request, its status and duration.
  4. The gateway dashboard. Find the API call made at that second and read its response.
grep "wc-ajax=checkout" /PATH/TO/access.log | tail -n 20

Where: server SSH. Placeholder: /PATH/TO/access.log is your site’s access log; ask your host if you cannot find it. For the block checkout, search for wc/store/v1/checkout instead. Good output: lines with status 200. Bad output: 403, 500 or 504 at the times shoppers complained. Some log formats include request time at the end of the line; a value close to your PHP max_execution_time or the proxy timeout means the request was killed for running too long.

If the trace stops at the step where the gateway is called and the gateway dashboard shows no request, the call never left your server (outbound firewall, DNS, or PHP timeout). If the dashboard shows a successful charge but the trace stops afterward, the payment worked and something after it crashed: look for a fatal error from a plugin hooked to order status changes, such as an invoicing, CRM or stock sync plugin.

When to contact your host

Open a ticket when the evidence points below WordPress: 403s you cannot trace to a plugin, 502 or 504 errors on checkout, outbound connection failures to the gateway, a wrong server clock, or a server cache you cannot configure. Include:

  • The exact time of a failed attempt in UTC, and the checkout URL.
  • The request path (/?wc-ajax=checkout or /wp-json/wc/store/v1/checkout), the status code and the response headers.
  • Any Cloudflare Ray ID shown on an error page or in the cf-ray header.
  • The relevant lines from fatal-errors, place-order-debug- or the gateway log, with customer details removed.
  • What changed recently: updates, new plugins, firewall or CDN changes.

The diagnostic tools page helps you collect headers first. If sales are stopped right now, 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