Storing Custom Data with woocommerce_add_cart_item_data
Whatever you return from this filter becomes part of the cart item's identity, is saved in the session with it, and is empty in the block add-to-cart request unless you fill it from the Store API request yourself.
woocommerce_add_cart_item_data lets you attach your own values to a cart item at the moment it is added, and whatever you return becomes part of that item's identity. WooCommerce applies it in WC_Cart::add_to_cart() with four arguments — the cart item data array, the product ID, the variation ID and the quantity — and then hashes the result into the cart item key. That single fact explains most of the surprises: when two adds merge, when they split, and why a value that should be a setting turned into a separate line.
The working shape
add_filter( 'woocommerce_add_cart_item_data', 'ti_store_engraving', 10, 4 );
function ti_store_engraving( $cart_item_data, $product_id, $variation_id, $quantity ) {
if ( empty( $_POST['ti_engraving'] ) ) {
return $cart_item_data;
}
$cart_item_data['ti_engraving'] = sanitize_text_field( wp_unslash( $_POST['ti_engraving'] ) );
return $cart_item_data;
}
Always return the array, even when you add nothing. The value is cast to an array after the filter, so returning null by accident does not crash — it silently drops every other plugin's data for that item, which is far harder to trace than an error.
Printing the input on the product page, displaying the value in the cart and copying it to the order are three further hooks. That end-to-end chain is covered in after add to cart button; this article is about what happens to the data in between.
The data becomes part of the cart item key
After the filter, generate_cart_id() builds the key from the product ID, the variation ID, the variation attributes and every key and value in your array, concatenated and passed through md5(). Then find_product_in_cart() looks for an existing line with that key.
The consequences are mechanical:
| What you store | What the cart does |
|---|---|
| Nothing | Adding the product again increases the quantity of the existing line |
| A value the customer chose, e.g. engraving text | Same text merges, different text creates a new line |
A value that changes every time, e.g. microtime() or a random token |
Every add creates a new line, even for identical choices |
A value that is the same for everyone, e.g. a flag 'source' => 'quiz' |
Merges with other quiz adds, splits from normal adds of the same product |
A random key is the usual way people force separate lines, and it is a legitimate choice for products where each unit is configured separately. It is a bug when it arrives by accident — a snippet that stores the time of adding "for reference" makes the quantity buttons on the product page useless, because every click is a new line.
Arrays and objects are flattened with http_build_query() for the hash, so nested data works, but two arrays with the same values in a different order produce different keys. Sort anything you build from user input before storing it.
Session persistence is automatic
Older tutorials pair this filter with woocommerce_get_cart_item_from_session to "restore" the data on the next page load. In current WooCommerce that is unnecessary for plain values: when the cart is saved, every key of each item is written to the session except data, the product object, and when it is loaded the stored keys are merged back alongside a fresh product. Your engraving text survives page loads, logins and the persistent cart without any extra code.
The session filter is still the right tool when something must be rebuilt rather than restored — an object, or a price derived from the stored value. Keep stored data to scalars and small arrays: the whole cart is serialised into the session row on every change, and a stored object or a long blob makes every page load of every shopper heavier.
The block add-to-cart trap
The filter also runs for the Store API, which is what block add-to-cart buttons use. But the Store API does not post the product form. Its add-item route builds the request with an empty cart_item_data array, and $_POST['ti_engraving'] is simply not there. A store that moved to block templates sees the field on the page and nothing in the cart.
The Store API exposes the original request through its own filter, available since WooCommerce 8.8:
add_filter( 'woocommerce_store_api_add_to_cart_data', 'ti_store_api_engraving', 10, 2 );
function ti_store_api_engraving( $add_to_cart_data, $request ) {
$engraving = $request->get_param( 'ti_engraving' );
if ( $engraving ) {
$add_to_cart_data['cart_item_data']['ti_engraving'] = sanitize_text_field( $engraving );
}
return $add_to_cart_data;
}
The value then flows through woocommerce_add_cart_item_data like any other, so validation and display code stays in one place. Whether your front-end sends ti_engraving in the request is a separate question for the block's JavaScript; a plain field printed by PHP next to a block button is not sent.
Validation belongs somewhere else
This filter is the wrong place to refuse an add to cart. On the classic form it runs after woocommerce_add_to_cart_validation but before the cart's own purchasable and stock checks; an exception thrown here does abort the classic add_to_cart(), whose try block turns the message into a notice, but that is a side effect of the surrounding code rather than a contract. Checks such as "engraving is required" or "maximum 20 characters" belong in woocommerce_add_to_cart_validation, which can refuse the add with a notice and is called by the Store API's add-item route as well as the classic form (there it runs after this filter and receives the cart item data). Store only what has passed.
Common mistakes
- Registering with the default
accepted_argsof 1 while the function declares four parameters, which throws a "Too few arguments" error on add to cart. - Forgetting to return the array in the early-exit branch.
- Storing unsanitised
$_POSTdata and printing it later in the cart and emails. - Expecting the value on the order without copying it there; for many fields, product add-ons do the whole chain.
Verify
- Add the product twice with the same value: one line, quantity two.
- Add it with a different value: a second line.
- Reload the cart and log out and back in: the value is still there.
- If the store uses block templates, repeat from the block product page.
Frequently asked
- Not for plain values. WooCommerce saves every key of the cart item to the session except the product object and merges them back on load. The session filter is only needed when you want to rebuild something on load, such as an object or a recalculated price.
- Because the cart item key is an md5 of the product, variation and every key and value you added. Different values mean different keys and separate lines. A value that changes on every request, like a timestamp, makes every add a new line.
- Block add-to-cart buttons use the Store API, which sends JSON rather than the product form, so $_POST does not hold your field. Read it from the request in woocommerce_store_api_add_to_cart_data instead.