GTM Kit Premium. Requires WooCommerce and GTM Kit 2.22.0 or later.
GTM Kit reports add_to_cart and remove_from_cart for the cart controls WooCommerce renders: the classic add-to-cart buttons and remove links, and the block-based Mini Cart, Cart and Checkout. A theme that changes the cart itself, with its own fetch calls to the WooCommerce Store API, triggers none of those paths, so those changes go unreported.
gtmkit.cart.sync() closes that gap. Hand it the cart response your Store API call returns and GTM Kit works out what changed since the cart it last reported, then sends the events: add_to_cart for every line that grew, remove_from_cart for every line that shrank or disappeared, each with the net quantity, its value, the store currency, and the item data GTM Kit attaches to every cart line. The events go through GTM Kit’s runtime, so consent gating and event deferral apply and they appear in the Event Inspector.
Turn it on
gtmkit.cart loads with the custom product list tracking API: GTM Kit → Commerce → WooCommerce: Event Customization → Custom product list tracking API. There is no separate setting. The gtmkit:lists:ready event tells you both APIs are installed.
Use it
Call sync() with the response of any wc/store/v1/cart* request. They all return the full cart.
Add to cart:
async function addToCart( productId, quantity ) {
const response = await fetch( '/wp-json/wc/store/v1/cart/add-item', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', Nonce: storeNonce },
body: JSON.stringify( { id: productId, quantity } ),
} );
const cart = await response.json();
if ( response.ok ) {
gtmkit.cart.sync( cart );
}
}
Change a quantity:
const cart = await ( await fetch( '/wp-json/wc/store/v1/cart/update-item', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', Nonce: storeNonce },
body: JSON.stringify( { key: lineKey, quantity: 3 } ),
} ) ).json();
gtmkit.cart.sync( cart );
If the line went from 1 to 3, that is one add_to_cart with quantity 2. From 3 to 1 is one remove_from_cart with quantity 2.
Remove a line:
const cart = await ( await fetch( '/wp-json/wc/store/v1/cart/remove-item', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', Nonce: storeNonce },
body: JSON.stringify( { key: lineKey } ),
} ) ).json();
gtmkit.cart.sync( cart );
A batch response (wc/store/v1/batch) works too: pass the whole response and every cart body inside it is processed in order.
sync() returns a promise resolving to { added, removed }, the items it reported, which is handy in a console.
List attribution
When the add came from a product list you have marked up for GTM Kit, pass the list context so the add_to_cart carries item_list_name, item_list_id and index:
gtmkit.cart.sync( cart, {
context: button, // any element inside the card, or a gtmkit.lists.contextFor() result
productId: productId, // the id you sent to the Store API; an array when a variation and its parent both apply
} );
Only the line for that product gets the list fields. Other lines added by the same request, such as accessories or an add-on, carry none; call sync() once per line with its own context if you want them attributed too. Removals never carry list fields. When you leave out productId and exactly one line was added, that line is attributed; when several were added, none are.
GTM Kit stores the list on the cart line with a request of its own, so begin_checkout, purchase and the server-side purchase can carry it later. That request does not hold up the events. If your theme sends the shopper to the checkout straight after the add, wait for it first:
await gtmkit.cart.sync( cart, { context: button, productId } );
await gtmkit.cart.whenSaved();
window.location.href = checkoutUrl;
whenSaved() resolves once every list captured so far is stored, or the attempt failed. It never rejects.
Combining with gtmkit.lists.addToCart()
If you already report an add through gtmkit.lists.addToCart(), keep doing so and hand the response to sync() as well. GTM Kit remembers the units the list API reported and subtracts them, so the add is sent once, with the list fields from the list API. Anything the list API did not report, such as an add-on the same request put in the cart, is still reported by sync().
How the baseline works
GTM Kit needs to know what the cart looked like before your change. Pages are often served from a full-page cache, so nothing about the visitor’s cart can be read from the page itself. Instead:
- When the browser has no cart cookie, the cart is empty.
- When the last reported cart was stored for the cart the browser has (matched on WooCommerce’s cart hash cookie), it is reused.
- Otherwise, for example on the first visit with a cart or after the cart changed in another tab, GTM Kit fetches the cart once and treats it as the starting point without sending anything.
A sync() call made before that starting point is ready is queued and reported once it is, so the first change on a page is not lost. Reloading the page, or opening a second tab, sends nothing.
If the cart cannot be fetched (the Store API is down or answers with an error), GTM Kit does not guess. The next cart you hand to sync() becomes the starting point and nothing is sent for it, so items the shopper already had are never reported as new adds. The change in that one response goes unreported.
On pages that also load GTM Kit’s own block tracking (a Mini Cart in a widget area is enough), both share the same “last reported cart”, so a change is reported once whichever side sees it first.
If your theme also hands the response to the block cart, with wp.data.dispatch( 'wc/store/cart' ).receiveCart( cart ), call sync() in the same step, before or after it, with no await in between. sync() then reports the change before the block tracking reads the cart, so the add keeps its list fields and an add you reported through gtmkit.lists.addToCart() is not sent again. When sync() runs later than the block tracking, the block tracking reports the change first, without list fields, and an add already reported through gtmkit.lists.addToCart() is sent a second time.
What it does not do
- It does not watch your
fetchcalls. You callsync(); nothing is reported until you do. - It does not cover the classic cart page. Quantity updates and removals that submit WooCommerce’s cart form or follow its remove links are reported by GTM Kit’s own WooCommerce script when that is loaded, not by
sync(). - It does not report
view_cart,begin_checkoutorpurchase. Those come from GTM Kit as usual.
Checking that it works
With Console log on (GTM Kit → Tools), every event is logged as GTM Kit: Pushing event …. A sync() call with something other than a Store API cart is reported once in the console. The Event Inspector shows each event with its full payload.