Skip to content
ThemesIonic — home
WooCommerce

Validating Add to Cart Requests in WooCommerce

Returning false stops the product entering the cart, but it does not explain anything. The notice is a separate call, and the filter is skipped entirely when a quantity is changed on the cart page, which is the gap most validation snippets fall through.

Updated 5 min read intermediate

The filter decides whether an item enters the cart; it does not decide what the shopper is told. woocommerce_add_to_cart_validation is applied by the add-to-cart handlers — the form handler, the AJAX handler and the Store API — just before they call WC_Cart::add_to_cart(), with a boolean that starts as true. Return false and the add is cancelled. Say nothing and the shopper watches the page reload with an empty cart and no explanation. The notice is a second, separate call, and the filter runs in only one of the three places a cart's contents can change.

The working shape

add_filter( 'woocommerce_add_to_cart_validation', 'ti_limit_per_order', 10, 5 );

function ti_limit_per_order( $passed, $product_id, $quantity, $variation_id = 0, $variations = array() ) {
	$limit = (int) get_post_meta( $product_id, '_ti_max_per_order', true );

	if ( $limit < 1 ) {
		return $passed;
	}

	$in_cart = 0;

	foreach ( WC()->cart->get_cart() as $item ) {
		if ( (int) $item['product_id'] === (int) $product_id ) {
			$in_cart += $item['quantity'];
		}
	}

	if ( $in_cart + $quantity > $limit ) {
		wc_add_notice(
			sprintf( __( 'You can order at most %d of this product.', 'ti' ), $limit ),
			'error'
		);
		return false;
	}

	return $passed;
}

Four things to keep. Return $passed rather than true on the success path, so a false from another plugin's callback earlier in the chain is preserved. Count what is already in the cart, because the filter sees only the quantity being added. Queue the notice before returning — wc_add_notice() with the error type is what the shopper sees, and it is the only thing they see. And keep the defaults on the last two parameters: the simple-product handler and the AJAX handler pass only three arguments, so a five-parameter function without defaults throws an ArgumentCountError on every simple add.

The arguments

Position Value Note
1 $passed Current verdict; return it unchanged to approve
2 $product_id Parent product for a variation; each child's ID for a grouped product
3 $quantity Being added now, not the cart total
4 $variation_id Passed by the variable handler and the Store API; absent from the simple and AJAX handlers
5 $variations Chosen attributes as attribute_pa_size => 'm'; absent likewise

Register the filter with five accepted arguments and default the last two. The common snippet registers three, never sees $variation_id, and treats every variation as the same product — the shape of many bug reports about rules that work on simple products and not on variable ones.

Where the filter runs and where it does not

The handlers that apply it cover the single product form, the shop grid's AJAX button, the Store API used by the block cart, and the cart being restored from the session on a later visit. Notices queued with wc_add_notice() surface in all of them; the Store API converts them into an error response the block shows inline.

What it does not cover:

  • Programmatic adds. A plugin calling WC()->cart->add_to_cart() directly bypasses the handlers and the filter with them. Any rule that must survive a third-party add-to-cart call needs the checkout check below.
  • Quantity changes on the cart page. That path runs woocommerce_update_cart_validation, with the cart item key, the cart item and the new quantity. A limit enforced only at add time is bypassed by adding one and editing the number.
  • Checkout. The cart is re-validated before the order is created through woocommerce_check_cart_items, where wc_add_notice() errors stop the order. Anything that could have changed since the add — stock, a time window, a per-customer limit of the kind wholesale rules impose — belongs there too. A rule that must hold needs all three. The add-to-cart filter is the friendliest place to tell the shopper early; the checkout check is the one that actually guarantees it.

Stock is not your job here

Stock and purchasability checks run inside add_to_cart() once your filter has passed. Re-implementing them is redundant and, for variable products, usually wrong — a rule that reads $product->get_stock_quantity() on the parent gets null for a variation-managed stock. If stock behaviour looks wrong, the setting is in inventory management rather than in validation.

Side effects to avoid

The filter runs during the add, which on the single product page is a form submission and on the grid is an AJAX request. Redirecting from inside it breaks the AJAX path and leaves the grid button spinning. Modifying the cart from inside it — removing another item so this one can be added — runs before the add completes and leaves the cart in a state the rest of add_to_cart() did not expect. Validate, notify, return. Anything structural belongs in woocommerce_add_to_cart, the action that fires once the item is in.

Verify

  • Add past the limit from the single product page: the page reloads with the error notice and the cart unchanged.
  • Add from the shop grid: the button shows the error rather than spinning forever.
  • Add one, then raise the quantity on the cart page: if it succeeds, woocommerce_update_cart_validation is still to be written.
  • Add, wait, change the rule, go to checkout: the checkout should refuse, or woocommerce_check_cart_items is missing.

A product that cannot be added at all, with no notice, points to a callback returning false unconditionally on a path you did not test — most often a rule written for variations that never considered the simple-product call, where the last two arguments are absent.

Frequently asked

Returning false only cancels the add. The message is yours to queue with wc_add_notice( $text, 'error' ) before returning. Without it the shopper sees the page reload and nothing else, which reads as a broken button.
Because the cart page's quantity update runs woocommerce_update_cart_validation, not the add-to-cart filter. A limit that must hold everywhere needs that filter too, plus woocommerce_check_cart_items for the checkout, where the cart is re-validated before the order is created.
The second argument is the parent product ID. For a variable product the variation ID is the fourth argument and the chosen attributes are the fifth, so a rule about a specific variation must read those, not the product ID.

Related guides