Skip to content
Blog

How to Add an "Order Again" Button to the WooCommerce Thank You Page

WooCommerce ships an “Order again” feature, but most store owners have never seen it. It lives on the My Account → View Order screen, it only appears on completed orders, and it sends the customer to the cart. The one place a repeat purchase is most likely to start — the order confirmation page a customer is already looking at — shows nothing.

This post adds that button with one hook, explains the two WooCommerce quirks that silently break the naive version, and — if you want it — sends the re-order straight to checkout instead of the cart. Everything below was written and tested against WooCommerce 11 on a block theme; it works on classic themes too.

The snippet

Drop this in a small plugin, an mu-plugin, or your child theme’s functions.php:

add_action( 'woocommerce_thankyou', 'myshop_order_again_button', 20 );
function myshop_order_again_button( $order_id ) {
	$order = wc_get_order( $order_id );

	if ( ! $order || ! is_user_logged_in() || ! current_user_can( 'order_again', $order->get_id() ) ) {
		return;
	}

	// Same filter WooCommerce's click handler checks, so the button never
	// renders for an order the handler would reject.
	$valid_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) );

	if ( ! $order->has_status( $valid_statuses ) ) {
		return;
	}

	$url = wp_nonce_url(
		add_query_arg( 'order_again', $order->get_id(), wc_get_cart_url() ),
		'woocommerce-order_again'
	);
	?>
	<p class="order-again">
		<a class="button wp-element-button" href="<?php echo esc_url( $url ); ?>">
			<?php esc_html_e( 'Order again', 'woocommerce' ); ?>
		</a>
	</p>
	<?php
}

// Core only allows "order again" on completed orders, but on the thank-you
// page the order is usually still processing — widen the list.
add_filter(
	'woocommerce_valid_order_statuses_for_order_again',
	function ( $statuses ) {
		return array_unique( array_merge( $statuses, array( 'processing', 'on-hold' ) ) );
	}
);

Clicking the button refills the cart with everything from the order — quantities, variations, the lot — using WooCommerce’s own re-order machinery, then lands the customer on the cart page ready to buy it all again.

Two parts of that snippet look optional and are not. They are the difference between a button that works and a button that renders never, or worse, errors when clicked.

Quirk one: your thank-you page order is not “completed”

WooCommerce’s re-order handler refuses any order whose status is not on an allow-list, and by default that list contains exactly one entry: completed. But an order being viewed on the thank-you page has almost never reached completed — a just-paid order is processing, a bank-transfer order is on-hold.

So a naive Order Again button on the thank-you page fails for essentially every visitor: either you render it anyway and the click bounces off the handler with an error notice, or you check the status first and the button never appears. Both failures are silent — nothing is logged, nothing looks broken.

The fix is the woocommerce_valid_order_statuses_for_order_again filter at the bottom of the snippet, which adds processing and on-hold to the allow-list. Note that the button-rendering code reads its status check through the same filter. That is deliberate: the render condition and the click handler can never disagree, so the button only ever appears where the click will succeed.

Quirk two: block themes — and why this hook still works

If your site runs a block theme, the order confirmation page is rendered by the Order Confirmation block, not the classic thankyou.php template, and the usual advice is that classic hooks are dead there.

For this hook, that advice is wrong. The Order Confirmation block deliberately fires the classic woocommerce_thankyou action for backwards compatibility, so the snippet above renders on block themes with no changes. (Payment plugins rely on the same behavior — it is how a gateway’s “complete your payment” box reaches the block-based confirmation page.) The button markup uses both button and wp-element-button classes so it picks up the theme’s button styling in either world.

If you would rather place the button precisely — say, between specific blocks in the Order Confirmation template — register a shortcode form and drop a Shortcode block wherever you want it:

add_shortcode(
	'order_again_button',
	function () {
		$order_id = absint( get_query_var( 'order-received' ) );

		if ( ! $order_id ) {
			return '';
		}

		ob_start();
		myshop_order_again_button( $order_id );

		return ob_get_clean();
	}
);

Who sees the button

The is_user_logged_in() and current_user_can( 'order_again', … ) checks mirror what WooCommerce’s handler enforces: re-ordering requires a logged-in customer who owns the order. Guests cannot use it — the handler rejects them regardless of what the button promises — so the snippet simply never shows them a button that would fail. If your store runs guest checkout, the button quietly appears only for account holders, which is the correct behavior rather than a bug.

The nonce in the URL is also load-bearing. WooCommerce validates it before touching the cart, so don’t be tempted to build the link by hand without wp_nonce_url().

Bonus: send the re-order straight to checkout

The stock behavior lands the customer on the cart. For a “buy the same thing again” flow, the cart is often a pointless stop — the customer already decided. You might expect to fix that by pointing the button’s URL at the checkout page instead. That does nothing: WooCommerce processes order_again while loading the cart session and then hard-redirects to the cart page — the redirect is hardcoded in WC_Cart_Session with no filter on it, whatever page the link targeted.

There is a safe way through, but it is genuinely two steps, and the obvious one-step version has a data-loss bug worth understanding. WooCommerce fires the woocommerce_ordered_again action while it is refilling the cart — before the refilled cart has been saved to the session. Redirect away inside that action and you can arrive at checkout with an empty cart: the redirect skipped the code that persists what was just rebuilt.

So instead: set a flag during the refill, let WooCommerce finish its own flow (persist the cart, redirect to the cart page), and forward to checkout on arrival:

// Step 1: while the cart is being refilled, remember where we want to end up.
add_action(
	'woocommerce_ordered_again',
	function () {
		if ( WC()->session ) {
			WC()->session->set( 'myshop_reorder_to_checkout', true );
		}
	}
);

// Step 2: on arrival at the cart page, forward to checkout — but only if
// the re-order actually put something in the cart.
add_action(
	'template_redirect',
	function () {
		if ( ! function_exists( 'is_cart' ) || ! is_cart() || ! WC()->session ) {
			return;
		}

		if ( ! WC()->session->get( 'myshop_reorder_to_checkout' ) ) {
			return;
		}

		WC()->session->set( 'myshop_reorder_to_checkout', null );

		if ( ! WC()->cart || WC()->cart->is_empty() ) {
			return; // Nothing survived the re-order — stay on the cart, where the notices explain why.
		}

		wp_safe_redirect( wc_get_checkout_url() );
		exit;
	}
);

The customer experiences one click and two invisible redirects: confirmation page → cart → checkout, with the cart fully populated. The empty-cart guard matters: if the products from the old order were deleted or went out of stock, WooCommerce leaves notices explaining what happened — on the cart page. Forwarding an empty cart to checkout would just bounce the customer back with no explanation, so in that case the snippet stays put and lets the notices do their job.

Where to put the code

Any of the standard homes work: a tiny custom plugin, an mu-plugin in wp-content/mu-plugins/, a code-snippets plugin, or a child theme’s functions.php. Prefer one of the plugin options if you can — the button belongs to the store, not the theme, and it should survive a theme switch.

One tuning note: the status filter above allows processing and on-hold. If your store sells things that shouldn’t be re-buyable while a previous order is still unpaid, drop on-hold from the list — the button and the handler will both respect whatever you decide, because they read the same filter.

That’s the whole feature: one action, one filter, and — if you want the checkout shortcut — one session flag. No plugin to install, and every moving part is WooCommerce’s own re-order machinery, so stock handling, variation data, and pricing all behave exactly as a fresh purchase would.

If the order you’re making re-buyable was paid through Stripe or Paddle, the checkout your customer lands on is the same one your payment gateway renders — this works unchanged with PimiPay’s Paddle and Stripe gateways, and if you’re still choosing between the two processors, our Paddle vs Stripe comparison covers the trade-offs.