WooCommerce Coupon Not Applying at Checkout Block: Fix

WooCommerce coupon not applying at the checkout block after WP 6.8? Fix REST permissions, cart token mismatches, and block validation with working code.
WooCommerceWordPressCheckout
May 2, 20265 min read973 words

The Problem

A client emailed me Friday: "Coupons stopped working at checkout this morning." Apply a code in the checkout block, the input clears, no error message, no discount. The same code works fine in the legacy shortcode checkout if you toggle it back. WooCommerce 9.6, WordPress 6.8, no recent code changes, but auto-updates pushed a wc-blocks update overnight.

If you are seeing the same thing (coupon field accepting input but silently dropping it, or showing a vague "Sorry, this coupon is not applicable" notice on a code that should work), the cause is almost always a mismatch between the Store API cart token and the coupon's REST permission check.

Why It Happens

The checkout block uses /wc/store/v1/cart/apply-coupon instead of the legacy AJAX action. Three things break this in 2026.

The cart token gets stripped. The Store API requires a valid Cart-Token header. WooCommerce generates one per session and includes it via the wp.apiFetch middleware shipped with @woocommerce/blocks. If a plugin overrode apiFetch.use() with a custom auth middleware (JWT, custom OAuth wrappers), the cart token gets stripped and the request comes through as anonymous. Anonymous carts can read but not mutate, so the apply call silently fails.

Custom validation hooks misread the cart. Custom coupon types from extensions (Smart Coupons, Advanced Coupons) often hook woocommerce_coupon_is_valid to add their own checks. In the legacy flow these run inside WC_Cart::add_discount() with full session context. In the Store API flow they run inside Automattic\WooCommerce\StoreApi\Routes\V1\CartApplyCoupon::get_response(), which does not load the global WC()->cart until later. Any check that calls WC()->cart->get_subtotal() returns 0, the minimum-spend rule fails, and the coupon is rejected.

Block validation rejects stale templates. WP 6.8 tightened block validation. If your child theme has a customized checkout-block.php template with stale data-block-name attributes, the editor renders fine but the front end fails the block_parser and the entire block tree falls back to read-only. The coupon input renders, but submission never fires.

The Fix

Step 1: Confirm it is a Store API problem. Open DevTools → Network, apply the coupon, look for apply-coupon. If you see a 400 with { "code": "woocommerce_rest_cart_invalid_token" }, it is the cart token. If you see a 400 with woocommerce_rest_cart_coupon_error and data.details mentioning subtotal or min spend, it is the validation context. If there is no request at all, it is block validation.

Step 2: Restore the Store API cart token. Most plugins that break this are auth wrappers. Add this filter to your child theme's functions.php to make sure the cart-token middleware runs before the broken auth wrapper:

add_action( 'wp_enqueue_scripts', function () {
    if ( ! is_checkout() && ! is_cart() ) {
        return;
    }

    $nonce = wp_create_nonce( 'wc_store_api' );

    wp_add_inline_script(
        'wc-blocks-checkout',
        sprintf(
            "wp.apiFetch.use( wp.apiFetch.createNonceMiddleware( '%s' ) );",
            esc_js( $nonce )
        ),
        'before'
    );
}, 5 );

The wp.apiFetch.use() call registers a fresh nonce middleware before any auth plugin's middleware loads, so the Store API gets the nonce and the cart cookie it expects. Priority 5 matters here. Anything later runs after the broken middleware and gets clobbered.

Step 3: Fix the custom coupon validation context. If your store uses Smart Coupons or a custom woocommerce_coupon_is_valid filter, wrap the cart call so it does not break in the Store API context:

add_filter( 'woocommerce_coupon_is_valid', function ( $valid, $coupon, $discounts ) {
    if ( ! WC()->cart || WC()->cart->is_empty() ) {
        $cart = $discounts->get_object();
        $subtotal = is_array( $cart ) && isset( $cart['cart_contents'] )
            ? array_sum( wp_list_pluck( $cart['cart_contents'], 'line_subtotal' ) )
            : 0;
    } else {
        $subtotal = WC()->cart->get_subtotal();
    }

    $min = (float) $coupon->get_minimum_amount();
    if ( $min > 0 && $subtotal < $min ) {
        return false;
    }

    return $valid;
}, 10, 3 );

The $discounts argument is a WC_Discounts object that holds the cart contents passed in from the Store API route. Reading subtotal from $discounts->get_object() works in both legacy and Store API flows, so the same hook works on both checkouts.

Step 4: Re-validate the checkout block template. From the WordPress admin go to Pages → Checkout and click the three dots → Replace blocks with a default Checkout. If you have customizations, copy them out first. The template that ships with WooCommerce 9.6 has the correct data-block-name attributes for WP 6.8's stricter parser. If you have forked the template in a child theme at woocommerce/checkout/checkout-block.php, delete it and re-copy from wp-content/plugins/woocommerce/templates/.

Step 5: Clear the persistent cart cache. WooCommerce caches the cart in wc_session_* rows in the wp_options and wp_woocommerce_sessions tables. After applying any of the fixes above, run this once via WP-CLI to purge stale tokens so existing visitors get a fresh session on the next page load:

wp wc session destroy --all --user=guest
wp transient delete --all

The WooCommerce Store API docs cover the full request/response shape if you need to dig into a specific 400 response.

The Lesson

The checkout block is stricter than the shortcode about session state and request shape. When coupons silently drop, look at the Store API response first — the error code tells you whether it is auth, validation, or block parsing in three seconds, and the fix is rarely on the JS side.

If your store is silently losing discount conversions right now, this is the kind of thing I unwind on client retainers. See my services, or if you want the related fix I wrote up the WooCommerce Checkout Block payment methods fix a couple of weeks back.

Back to blogStart a project