JavaScript Events
The Pixel Manager exposes a JavaScript event API, on its own event bus (pmw.bus, version 1.65.0 and newer) and on jQuery's event system, which keeps working indefinitely. It goes in two directions:
- Listen to
pmw:event:*events to react to everything the Pixel Manager tracks, with the fully processed payload. - Trigger a
pmw:*event to tell the Pixel Manager that something happened that it could not detect on its own.
Both are supported entry points for your own code and for third party plugins.
The pmw:* entry events have existed for a long time. The pmw:event:* listener API was introduced in version 1.52.1, the pmw.bus API in 1.65.0, and since 1.66.0 an entry event dispatched before the Pixel Manager finished loading is held instead of dropped.
Which direction do you need?
| You want to | Use | Example |
|---|---|---|
| Send data to another system whenever the Pixel Manager tracks something | Listen to pmw:event:* | Push every purchase into your own analytics endpoint |
| Add a pixel or a conversion the Pixel Manager does not cover | Listen to pmw:event:* | Fire a partner network tag on purchase |
| Make the Pixel Manager track an interaction it cannot see | Trigger pmw:* | A headless or custom-built add-to-cart button |
| Adjust the data before it reaches a platform | Neither, use Event Filters | Rewrite the product ID sent to Meta |
If you only want to change data that the Pixel Manager already sends, use Event Filters instead. Filters run inside the pipeline and modify the payload. The events on this page run around it.
Listening to events
Listen to pmw:event:<event-name>. The event name is the tracking event with underscores replaced by hyphens, so add_to_cart becomes pmw:event:add-to-cart.
Since version 1.65.0 the recommended way is the Pixel Manager's own event bus:
pmw.bus.on("pmw:event:purchase", function (payload) {
console.log(payload.event); // "purchase"
console.log(payload.event_data); // the order, product, cart, etc.
console.log(payload.context); // url, referrer, page_type, consent, ...
console.log(payload.pixels); // the data adapted per pixel
console.log(payload.firing); // which pixels fired browser-side and server-side
});
The jQuery form is the original one and stays supported indefinitely. Use it if you need to support versions older than 1.65.0. Note that jQuery hands your callback (event, payload), while pmw.bus.on() hands it the payload directly:
jQuery(document).on("pmw:event:purchase", function (event, payload) {
console.log(payload.event);
});
These events fire after the Pixel Manager has processed the event, applied all filters and decided which pixels fire. That makes them the right place to read final, authoritative data.
Registration timing
Most events only reach the handlers that exist at the moment they are emitted. The purchase event is the exception, because it fires exactly once per order and is then locked out permanently, a page reload included. An integration that registered too late would lose the conversion with no second chance.
From version 1.65.0 on, pmw:event:purchase and the entry event pmw:purchase are therefore replayable: their last payload is retained, and a handler registered through pmw.bus.on() after the emit receives it immediately. No other event is replayed, and the replay lives on the bus only. A handler bound with jQuery(document).on() or with document.addEventListener() never hears anything that fired before it was bound.
Register your handlers from the command queue regardless. It is the only form that is also safe with the jQuery API and on versions older than 1.65.0.
The purchase event does not only fire on the order confirmation page. Automatic Conversion Recovery (pro) fires it on an ordinary shop page when a customer whose order was not tracked returns, and it fills the same order object. A snippet that is printed on the confirmation page only misses exactly those orders, which are the ones a recovery was supposed to win back. The event is the gate, so it is safe to load your code everywhere.
Available events
Every event the Pixel Manager processes is dispatched:
| Event | Fires when |
|---|---|
pmw:event:page-view | A page is viewed |
pmw:event:view-item | A product is viewed |
pmw:event:view-item-list | A product list becomes visible |
pmw:event:view-category | A product category page is viewed |
pmw:event:select-item | A product in a list is clicked |
pmw:event:search | A search results page is viewed |
pmw:event:add-to-cart | A product is added to the cart |
pmw:event:remove-from-cart | A product is removed from the cart |
pmw:event:view-cart | The cart is viewed |
pmw:event:add-to-wishlist | A product is added to a wishlist |
pmw:event:begin-checkout | The checkout starts |
pmw:event:add-shipping-info | A shipping method is selected |
pmw:event:add-payment-info | A payment method is selected |
pmw:event:place-order | The order button is clicked |
pmw:event:purchase | The purchase confirmation page is reached |
pmw:event:login | A customer logs in |
pmw:event:account-created | A customer account is created |
The payload
| Key | Contents |
|---|---|
event | The event name in its canonical form, e.g. add_to_cart |
event_data | The core data of the event: product, order, and so on. Empty for events that carry no data of their own |
context | timestamp, url, referrer, user_agent, page_type, user_id and the full consent state |
pixels | The event data adapted to each active pixel's own format |
firing | Per pixel, whether it fired in the browser and whether it fired server-side |
Respect consent
The payload is dispatched regardless of the visitor's consent state, so that your code can make its own decision. payload.context.consent tells you what the visitor allowed:
pmw.bus.on("pmw:event:purchase", function (payload) {
// Only act if the visitor accepted marketing cookies
if (!payload.context.consent.categories.marketing) return;
// Your code here
});
If your code sets cookies or sends personal data to a third party, you are responsible for honoring consent yourself. The Pixel Manager only applies consent to its own pixels.
It is tempting to skip the event and read pmwDataLayer.order from an inline script on the confirmation page. Do not. The event is what carries the Pixel Manager's guarantees: it fires once per order, it also fires for orders recovered by ACR, and it hands you the processed payload. The data layer object is simply present on the page, so code that reads it directly fires on every reload, never sees a recovered order, and has no consent state to consult.
Consent is the one thing the event does not decide for you. Your own tag fires outside the Pixel Manager's pipeline, so the consent check belongs in your handler.
Triggering events
Dispatch pmw:<event-name> when the Pixel Manager cannot detect an interaction on its own. These are the entry events: the same entry point the Pixel Manager's own listeners use, so the event goes through the entire pipeline, with filters, all active pixels, server-side tracking and consent handling included.
Since version 1.65.0 the recommended way is the Pixel Manager's own event bus:
pmw.bus.emit("pmw:add-to-cart", product);
The jQuery form is the original one and stays supported indefinitely. Use it if you need to support versions older than 1.65.0:
jQuery(document).trigger("pmw:add-to-cart", product);
Both take the same event names and the same arguments, and both end up in the same place. Note the difference in the handler signature when you listen: pmw.bus.on() hands your callback the payload directly, jQuery hands it (event, payload).
Available events
| Event | Argument | Notes |
|---|---|---|
pmw:add-to-cart | product object | See Building a product object |
pmw:remove-from-cart | product object | |
pmw:view-item | product object, optional | |
pmw:view-item-list | product object, optional | |
pmw:view-category | product object, optional | |
pmw:select-item | product object | |
pmw:add-to-wishlist | product object | |
pmw:search | none | Reads the search term from the page |
pmw:view-cart | none | |
pmw:begin-checkout | none | Deduplicated against the Pixel Manager's own checkout triggers |
pmw:add-shipping-info | {shippingTier: {slug, text}} | |
pmw:add-payment-info | {paymentType: {slug, text}} | |
pmw:place-order | none | |
pmw:purchase | none | Reads the order from the data layer |
pmw:login | none | |
pmw:account-created | none |
Do not trigger pmw:purchase to report an order the Pixel Manager did not already know about. It reads the order from pmwDataLayer.order, which is written by the plugin on the purchase confirmation page, and it does not bypass the order duplication prevention. Reporting purchases from your own code is not a supported path.
When to dispatch them
The Pixel Manager only reacts to an entry event once it has registered its own listeners and loaded the pixels, which happens at the end of its startup: it waits for jQuery, for the data layer, and for your consent management platform before any of that.
From version 1.66.0 on, that no longer matters. An entry event that arrives while the Pixel Manager is still starting up is held and processed as soon as it is ready, in the order the events were dispatched. You can dispatch one at any point after pmw.bus exists.
Entry events dispatched before the Pixel Manager had finished loading were silently dropped. Nothing was logged and no pixel fired, which looks exactly like a wrong payload or a wrong event name.
Snippets that report an interaction the theme or a checkout plugin renders early are the typical victim, because they fire on the shop's own "template rendered" event, long before the Pixel Manager is ready. On those versions, wait for pmwLoad before dispatching:
document.addEventListener("pmwLoad", function () {
jQuery(document).trigger("pmw:add-shipping-info", {shippingTier: {slug: "flat_rate", text: "Flat rate"}});
});
Note that the command queue does not help here: it runs before the entry-event listeners are registered. It is the right wrapper for registering your own listeners and filters, not for dispatching an entry event on those versions.
If your code needs to know whether the Pixel Manager has already registered its listener for an event, ask it (version 1.65.0 and newer):
if (pmw.bus.hasListeners("pmw:add-to-cart")) {
// The Pixel Manager is listening for this event.
}
On 1.66.0 and newer you do not need this check for dispatching. It is useful on older versions, and when your own code wants to skip building a payload nobody will read.
There is one thing no version can hold: an event dispatched before the Pixel Manager's script has run at all. pmw.bus does not exist yet at that point, so your code would throw. Wrap it in the command queue, which runs your function whenever the Pixel Manager gets there, and let the Pixel Manager hold the event from then on.
An entry event is a report of something that happened, not a request to fire a pixel twice. The Pixel Manager collapses a shipping tier reported by both your snippet and its own trigger into one add_shipping_info, and pmw:begin-checkout is deduplicated against its own checkout triggers. The other events are passed through as dispatched, so dispatch each interaction once.
Building a product object
The product argument is not free-form. Build it with the Pixel Manager's own helper so it carries every field the pixels expect:
let product = pmw.getProductDetailsFormattedForEvent(productId, quantity);
If the product is not in the data layer yet, because it was never rendered on the page, fetch it from the server first:
if (!pmwDataLayer.products[productId]) {
await pmw.getProductsFromBackend([productId]);
}
Complete example: a custom add-to-cart button
This is how the Pixel Manager's own Doofinder integration works. It listens to the search plugin's event and hands the product to the Pixel Manager:
document.addEventListener("doofinder.cart.add", async function (event) {
const {item_id, amount} = event.detail;
// Make sure the product is in the data layer
if (!pmwDataLayer.products[item_id]) {
await pmw.getProductsFromBackend([item_id]);
}
if (!pmwDataLayer.products[item_id]) return;
const product = pmw.getProductDetailsFormattedForEvent(item_id, amount);
if (!product) return;
pmw.bus.emit("pmw:add-to-cart", product);
});
Trigger the event only for the tracking. If the product was already added to the WooCommerce cart, either by your own code or on the server, do not call pmw.addProductToCart() as well. That would add it a second time in the Pixel Manager's own cart state.
Load order
Your listeners have to be attached before the Pixel Manager fires its events, and your code must not run before pmw exists. Both are solved by the command queue:
window._pmwq = window._pmwq || [];
window._pmwq.push(function () {
pmw.bus.on("pmw:event:add-to-cart", function (payload) {
// Your code here
});
});
This works no matter when the Pixel Manager loads, which matters on shops that delay or lazy load JavaScript.
The queue is also the safe place to dispatch an entry event from, on version 1.66.0 and newer: the queue guarantees that pmw exists, and the Pixel Manager holds the event until its listeners and pixels are live. See When to dispatch them for what to do on older versions.
Lifecycle events
Besides the tracking events, the Pixel Manager dispatches a few events about its own state. They fire in this order:
| Event | Fires when |
|---|---|
pmw:load-pixels | All active pixels have been loaded and are about to initialize |
pmwLoad | The Pixel Manager finished loading and pmw is available |
pmw:ready | The page has finished loading and the Pixel Manager is fully operational |
These three are native browser events dispatched on document, so they work with addEventListener, and from version 1.65.0 they are on the event bus as well:
document.addEventListener("pmwLoad", function () {
// pmw is available from here on
});
pmw.bus.on("pmwLoad", function () {
// pmw is available from here on
});
:::warning Do not use jQuery for the lifecycle events
jQuery's .on() picks these up too, because they are dispatched on document. Do not rely on it: on versions 1.65.0 to 1.66.0 a jQuery handler on a lifecycle event runs twice per page load, and if it calls a Pixel Manager function that fires an event, it can send the page into an endless loop that ends in RangeError: Maximum call stack size exceeded. Both were fixed in 1.66.1, but addEventListener and pmw.bus.on() were never affected on any version.
:::
The lifecycle events carry no payload, and they are not replayed. A handler registered after one of them has fired never runs, and nothing is logged. That makes them the wrong gate for anything that must not be missed.
The classic failure: an inline footer snippet checks whether the Pixel Manager is ready, finds that it is not, and falls back to waiting for pmw:ready. On a shop that defers or delays JavaScript, the Pixel Manager's bundle runs after that inline script, so the fallback registers too late every single time. It is not a race that sometimes works.
Run your code from the command queue and hang your conversion on pmw.bus.on("pmw:event:purchase", …) instead. Both are load-order proof.
For your own code, prefer the command queue over pmwLoad. The queue also runs your function when the Pixel Manager loaded before your script did, which pmwLoad does not, because the event has already fired by then.
Debugging
Turn on the console logger by loading any page with ?pmwloggeron. It prints a line for every event that enters and leaves the pipeline, which tells you immediately whether your triggered event arrived:
Pixel Manager: pmw:add-to-cart event fired
Pixel Manager: Processing event: add_to_cart
Pixel Manager: Public event dispatched: pmw:event:add-to-cart
If your event arrived during startup, you see it being held first, and the delivery once the Pixel Manager is ready:
Pixel Manager: pmw:add-to-cart arrived while the library was still loading, held until it is ready
Pixel Manager: delivering 1 entry event(s) that arrived while the library was still loading
Pixel Manager: pmw:add-to-cart event fired
If neither line appears, the event never reached the Pixel Manager: check the event name and, for the jQuery form, that jQuery is loaded at that point.
Switch it off again with ?pmwloggeroff.
Stability
The event names and the payload structure on this page are a public API and we keep them stable. Two notes:
- Internal
pmw:pixel:*andpmw:s2s:*events also exist. They are the plumbing between the pipeline and the individual pixels. Do not build on them, they change with the pixel implementations. - New keys may be added to the payload. Read the keys you need instead of assuming a fixed shape.