Tracking & Attribution

How Do You Track a WooCommerce Subscriptions Business End to End?

WooCommerce renewals fire from cron with no browser, so pixels miss them. Capture click IDs at first touch, ingest subscriptions server-side, count once.

Quick answer

Treat it as two connected jobs. Acquisition: capture every click ID (gclid, gbraid, wbraid, fbclid) at the first pageview, persist it server-side, and attach it to the customer at signup. Recurring: ingest the subscription itself as a durable record — status, billing period, recurring total, renewal ledger — fed by server webhooks plus a periodic census, never by browser pixels. Renewals fire from WordPress cron with the browser closed, so no client-side tracker can ever see them. Classify every order as initial, renewal, or refund before anything reaches an ad platform.

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Track a WooCommerce subscriptions business by treating it as two connected jobs, not one. Acquisition: capture every click ID — gclid, gbraid, wbraid, fbclid — on the first pageview, persist it server-side, and attach it to the customer when they sign up. Recurring: ingest the subscription itself as a durable record — status, billing period, recurring total, renewal ledger — fed by server webhooks and a periodic census, never by browser pixels. Renewal payments fire from WordPress cron with the customer’s browser closed, so no client-side tracker can ever see them.

The reason it feels like nothing on the market connects to your process: the tools are split down the middle of the question you’re actually asking. Pixel plugins know orders and know nothing about subscription state. Subscription analytics tools know MRR and churn and nothing about where the subscriber came from. Your question — which ad created this subscriber, and what did that subscriber become worth? — sits exactly on the seam, and buying one tool from each category doesn’t produce the join. It produces two dashboards that disagree.

This article maps each failure to its mechanism, settles the renewals-to-ad-platforms question that most guides get backwards, and shows how PartialLeads assembles the pipeline.


Why does subscription tracking break where one-off ecommerce doesn’t?

A one-off store has one moment that matters and a browser present for it. A subscription store has a lifecycle, and the browser is absent for most of it. Five mechanisms, and most stores have all five at once:

1. The renewal has no browser. WooCommerce Subscriptions generates renewal orders from wp-cron or a payment-gateway webhook — no page load, no JavaScript, no pixel, no GA4 client_id, no cookies, no UTMs. Every browser-dependent tracker records exactly $0 for renewal revenue. A subscriber paying $29/month who stays fourteen months is recorded, forever, as a $29 customer. Your acquisition channels look unprofitable because you’re comparing full ad spend against the first payment only.

2. The checkout you instrumented moved out from under you. WooCommerce’s Cart and Checkout Blocks run on the Store API and the @wordpress/data store, not the legacy jQuery events (added_to_cart, woocommerce_checkout_init) that pixel plugins were built to listen on. Plugins still bound to the old hooks stop firing AddToCart and InitiateCheckout — while Purchase keeps firing through the woocommerce_thankyou PHP hook. The result is the worst kind of break: the funnel goes half-dead, not obviously dead, so nobody investigates for months. High-Performance Order Storage compounds it by moving order data out of the WordPress post tables, breaking anything that reads orders through post-meta calls instead of the order CRUD API.

3. A subscription is a state machine, not an event. pendingactiveon-holdpending-cancelcancelled or expired, plus trials, plan switches, and failed payments. None of those are “conversions” and no pixel has a vocabulary for “went on-hold after a card declined.” Which means the numbers that actually run the business — MRR, churn, tenure, lifetime value — cannot be derived from event tracking at all, no matter how good the events are. Fixing your pixel does not get you MRR. They are different problems that happen to live in the same store.

4. Order totals hide the two revenue types. To WooCommerce’s own Analytics, a renewal order and a new-customer order are both orders. Sum them and “revenue” blends acquisition with retention — two numbers that routinely move in opposite directions. This is how a store holds flat topline for two quarters while new-subscriber acquisition quietly collapses underneath, propped up by an existing base that hasn’t churned yet.

5. Attribution dies exactly where it becomes valuable. The click ID that created the subscriber exists on the first pageview. The value of that subscriber accrues over the following year. Even setups that do capture gclid at signup rarely persist it anywhere durable enough to answer “what is a Meta-sourced subscriber worth versus a Google-sourced one, twelve months in?” — which is the only version of the question worth budgeting against.


Why doesn’t any single plugin close the gap?

Because the market has two categories and your question needs both:

Tracking and pixel plugins — the Meta and Google integrations, GTM-based setups, the various pixel managers — send events to ad platforms and analytics. They are order-shaped. They have no concept of subscription state, no MRR, no churn, no tenure. At best they fire a Purchase when a renewal lands, which creates its own problem (next section).

Subscription analytics tools — Metorik, Putler, and similar — give you genuine MRR, churn, cohorts, and LTV. They read your store, not your traffic. They can tell you a subscriber is worth $348 and cannot tell you which campaign made them.

Neither category is badly built. They’re built for adjacent problems, and the seam between them is exactly where a subscription business’s economics live. Running one from each column leaves you reconciling by hand: exporting subscribers from one tool, matching them by email against ad-platform reports in a spreadsheet, and re-doing it every month. That manual join is the “no system connects to my process” feeling, and it’s a real structural gap, not a research failure on your part.

The join requires one system holding four things simultaneously: the ad click, the signup, the subscription state, and the renewal ledger — with a durable identity threading them together.


What does the clean architecture look like?

Eight requirements. The first three are attribution, the next three are the subscription entity, the last two are the discipline that keeps the numbers honest.

  1. Capture identifiers at first touch, server-side. On the first pageview of any entry page, capture gclid, gbraid, wbraid (the iOS variants most hand-built captures ignore), fbclid, _fbp, _fbc, and UTMs — and persist them against a durable visitor identity rather than a page-scoped variable that dies at the next step.
  2. Bind identity at signup. When the customer checks out, the stored identifiers attach to their email and phone. This is the join key for everything that happens afterward, including payments that arrive months later.
  3. Stamp the visitor onto the order itself. An order carrying the visitor ID matches its originating session exactly, instead of being guessed at by email or IP after the fact. It must work on both classic and block/Store-API checkout, and declare HPOS compatibility.
  4. Model the subscription as an entity, not a stream of events. Store WooCommerce’s raw status verbatim — no lossy mapping — plus one derived active flag with a single shared definition used by the entity, the reports, and the reconciliation job alike. Two definitions of “active” in one codebase is how dashboards start disagreeing with each other.
  5. Feed that entity three redundant ways. Live subscription webhooks for immediacy, a one-time history sync so the record doesn’t start empty on the day you install, and a recurring status census that self-heals anything the live path missed. Webhooks get dropped and wp-cron gets skipped on low-traffic stores; a periodic full census is the only thing that makes the data eventually correct rather than optimistically correct.
  6. Classify every order: initial, renewal, or refund. This single field is what lets you separate acquisition revenue from retention revenue, and it’s what gates the decision in the next section.
  7. Compute MRR from the actual recurring total, normalized to a month, excluding trials — and never sum across currencies. A $120 annual plan is $10 of MRR. A discounted subscription is worth its discounted total, not the product’s list price. A trialing subscriber is not MRR until they pay. Multi-currency stores that cross-sum are reporting a number that means nothing.
  8. Gate immature metrics instead of guessing them. Lifetime value for ended subscriptions is an actual measured number. Lifetime value for active ones is an estimate that requires enough history and observable churn to be worth printing. A system that shows a confident LTV in week two is lying to you.

The genuinely hard parts, ranked honestly: identifier persistence across steps and subdomains is the hard one; the subscription entity plus its reconciliation is a real build; and the events layer is fiddly but well-documented. The recurring risk with any hand-built version is the same — it has to stay correct through theme changes, plugin updates, WooCommerce releases, and every new funnel page.


Should renewals be sent to Meta and Google as conversions?

The prevailing advice says yes — feed renewals into the Conversions API so bidding optimizes toward lifetime value. There’s a real counter-case, and most stores have never consciously chosen a side.

The case against firing renewals as Purchase conversions: the ad caused the acquisition. It did not cause month seven’s renewal. Send renewals into the same Purchase event your campaigns optimize on, and reported ROAS climbs every month without a single additional ad-driven sale — the original click keeps getting re-credited for revenue that retention produced. Optimization then learns from a signal that inflates over time and is worst exactly where your product is best. A store with strong retention will read as having brilliant ads.

The case for: value-based bidding genuinely does perform better when it can see downstream value, and for high-LTV products the first payment badly understates a customer’s worth.

Both are defensible. What isn’t defensible is not knowing which one your stack is doing today — and most WooCommerce stores running server-side purchase events have never checked whether renewal orders are in the stream. Check that first. If you want value-based bidding on lifetime value, send it as a separate, differently-named event so it never contaminates the acquisition signal your campaigns optimize on. Never route renewals through the same Purchase event.

The separate, universal requirement: deduplicate. If a browser pixel and a server event both describe the same purchase, they must carry the same event_id or Meta counts both. Duplicate purchase events are the most common cause of ROAS that looks great and reconciles against nothing.


How does PartialLeads track a WooCommerce subscriptions business?

PartialLeads is built around exactly the join described above — ad click, signup, subscription state, renewal ledger, one identity.

The WordPress plugin handles the parts that break. It injects the capture script site-wide and stamps the PartialLeads visitor ID onto every order — on classic and block/Store-API checkout, with HPOS declared — so a purchase matches its originating visitor at the highest confidence tier instead of being guessed at by email or IP. Order webhooks auto-register with the correct shared secret from a single pasted connection code. Add-to-cart capture is deliberately transport-agnostic: classic AJAX, classic form submit, blocks/Store-API fetch to cart/add-item (and /batch), the native wc-blocks_added_to_cart event, custom theme AJAX actions, and a capture-phase click listener as the universal fallback — deduplicated so overlapping mechanisms count once. That list exists because the blocks migration is precisely what silently killed AddToCart in browser-bound plugins.

Purchase never fires from the browser. Funnel events (product_viewed, product_added_to_cart, checkout_started) dispatch server-side to Meta, Pinterest, and TikTok as ViewContent / AddToCart / InitiateCheckout, fire-and-forget so the storefront never blocks. The Purchase conversion is webhook-driven and authoritative — the plugin never sends a client-side Purchase, so there is no browser/server double-count to reconcile.

Subscriptions are ingested three ways, exactly as the architecture requires. Native subscription.created/updated/switched/deleted webhook topics auto-register over the same signed route; a resumable “Sync subscription history” button backfills everything that existed before install (safe to re-click — the server upserts idempotently); and a daily background census reconciles anything a live event missed. Out-of-order events are handled by comparing the subscription object’s own modification timestamp rather than trusting arrival order. Stores without WooCommerce Subscriptions are completely unaffected — every entry point gates on the extension being active, and topic registration degrades silently rather than fataling.

The subscription record follows the discipline above. WooCommerce’s raw status is stored verbatim; is_active has one shared definition (active or pending-cancel — a pending-cancel member has paid for and still has access to the current period, so they count until WooCommerce actually ends them). MRR normalizes each subscription’s actual discounted recurring total to a month, excludes trials, and is never summed across currencies. Every order is classified initial, renewal, or refund.

Renewals are captured for reporting and suppressed from ad platforms by default. Renewal orders do not fire to Meta, Pinterest, or TikTok CAPI, and do not enter the Google Sheets offline-conversion path — a deliberate default so recurring revenue can’t inflate ad-reported ROAS. Outbound webhooks to your own systems still receive them, so your internal reporting sees everything.

The reports answer the business questions, not the tag questions. New subscribers, cancellations, net change, active count, churn rate, MRR, revenue per subscriber, and subscription revenue split from other revenue — each period-over-period against a comparable prior window. Broken out per product tier and per billing plan (Monthly / Quarterly / Annual), with average tenure for active subscribers, median tenure for ended ones, actual lifetime value for ended subscriptions, and an estimate for active ones that is gated and labelled “maturing” until there’s enough history to mean anything. The Subscriber Directory lists actual customers — status, plan, renewals, total paid, next payment — and each row links through to the captured lead session, where the journey and its originating click live. Campaign-level revenue, first-touch and last-touch side by side, sits in the Attribution report, with ROAS where Meta ad spend is connected.

Honest boundaries. Subscription ingestion covers WooCommerce Subscriptions today; the entity is source-agnostic by design but Stripe subscriptions aren’t wired into it yet. MRR-broken-down-by-acquisition-campaign is not a single pre-built column — the join exists through the subscriber’s linked lead session and the Attribution report covers campaign revenue, but that specific cross-tab is a roadmap item, not a screenshot. Subscription history begins at connect plus history sync, so the first weeks of trend lines are shorter than they’ll eventually be. Product tiers are parsed from product names (Silver/Gold/Platinum keywords, with grouping by product name when a catalog uses no level keywords) — a per-store keyword config is a future generalization. And an active-subscription LTV estimate stays hidden until the history supports it, which is a deliberate choice to under-promise.

Broken today PartialLeads mechanism Where you see it working
Renewals invisible to pixels (no browser) Server-side ingestion via subscription webhooks + history sync + daily census Renewal counts and total-paid per subscriber in the Subscriber Directory
Blocks/Store-API checkout killed AddToCart Transport-agnostic capture across classic AJAX, form submit, Store-API fetch, blocks event, click fallback AddToCart present again in Events Manager for block themes
Orders match to sessions by guesswork Visitor ID stamped onto the order (classic + blocks, HPOS-declared) Highest-confidence match on the purchase, not an email/IP guess
No MRR, churn, tenure, or LTV anywhere store_subscriptions entity with one shared active rule + trial exclusion Subscriptions report: MRR, churn %, net change, tenure, CLV per tier
Acquisition and retention revenue blended Every order classified initial / renewal / refund Subscription revenue split from other revenue, per period
Renewals inflating reported ROAS Renewals suppressed from Meta/Pinterest/TikTok CAPI + Google offline path by default Platform-reported conversions track new subscribers, not the base
Two dashboards that don’t reconcile One system holding click, signup, subscription, and renewals Subscriber row → linked lead session → originating campaign

What stays manual regardless of tooling: auditing and removing stray tags from old accounts, and deciding your own renewal policy for value-based bidding. What stops being a project is the pipeline. The free tier (50 leads) is enough to run the whole verification: place a test order from a URL with a known click ID, watch it match, then run the history sync and confirm your existing subscribers appear with the right statuses and MRR.


Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Sources

  1. WooCommerce — Subscriptions documentation: https://woocommerce.com/document/subscriptions/
  2. WooCommerce Developer Docs — High-Performance Order Storage (HPOS): https://developer.woocommerce.com/docs/features/orders/high-performance-order-storage/
  3. WooCommerce Developer Docs — Store API: https://developer.woocommerce.com/docs/apis/store-api/
  4. WooCommerce — Webhooks: https://woocommerce.com/document/webhooks/
  5. Meta for Developers — Deduplicate Pixel and Server events: https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events
  6. Google Ads Help — About offline conversion imports: https://support.google.com/google-ads/answer/2998031
  7. Google Analytics — Measurement Protocol for GA4: https://developers.google.com/analytics/devguides/collection/protocol/ga4

Frequently asked questions

QWhy doesn't Google Analytics show my WooCommerce subscription renewals?
Because GA4's ecommerce tracking runs in the customer's browser, and a renewal has no browser. WooCommerce Subscriptions generates renewal orders from wp-cron or a gateway webhook — the customer is asleep, the tab is closed, and the tracking script never executes. GA4 therefore records only the first payment of every subscription. A $29/month subscriber who stays a year appears as $29 rather than $348, which understates every paid channel that acquired them. The only fixes are server-side: the Measurement Protocol for GA4, or ingesting renewals directly into a system that reads your store rather than your browsers.
QWhat's the difference between a subscription tracking plugin and a subscription analytics plugin?
They solve adjacent halves of your problem. Tracking plugins send events to ad platforms and analytics — they're order-shaped and have no concept of subscription status, MRR, or churn. Analytics tools (Metorik, Putler and similar) compute genuine MRR, churn, cohorts, and LTV, but read your store rather than your traffic, so they can't tell you which campaign produced a subscriber. Neither is poorly built; the gap between them is where subscription economics actually live. Answering "which ad created this subscriber and what did they become worth" requires one system holding the ad click, the signup, the subscription state, and the renewal ledger together.
Q Should I send WooCommerce renewal orders to the Meta Conversions API?
Not through the same Purchase event your campaigns optimize on. The ad caused the acquisition, not month seven's renewal — routing renewals into Purchase means reported ROAS rises every month with no additional ad-driven sales, and bidding then learns from a signal that inflates over time. If you want value-based bidding to see lifetime value, send it as a separately-named event so the acquisition signal stays clean. PartialLeads' default is to capture renewals for reporting and suppress them from Meta, Pinterest, TikTok, and the Google offline-conversion path, while still passing them to your own outbound webhooks.
QWhy did my AddToCart events stop firing after switching to WooCommerce Blocks?
The Cart and Checkout Blocks run on the Store API and the @wordpress/data store, not the legacy jQuery events (added_to_cart, woocommerce_checkout_init) that older pixel plugins hook. Anything bound to those hooks goes quiet. Purchase usually keeps working because it fires from the woocommerce_thankyou PHP hook, which is why the break is easy to miss — the funnel goes half-dead rather than obviously dead. A durable fix listens transport-agnostically: classic AJAX, classic form submit, Store-API fetch to cart/add-item, the native wc-blocks_added_to_cart event, and a capture-phase click fallback, deduplicated so overlaps count once.
QHow do I attribute a subscriber to the ad that created them months later?
Persist, don't re-read. Capture the click IDs at the first pageview and store them server-side against a durable visitor identity; bind that identity to the customer's email and phone at signup; stamp the visitor ID onto the order itself so the purchase matches its session exactly rather than being guessed at by email or IP. From then on the subscription record and every renewal are reachable from the originating click by a stored join, so how much time passes and how many devices the customer changes stops mattering. Nothing about this depends on a cookie surviving.
QCan I get MRR and churn out of WooCommerce Analytics?
No — WooCommerce Analytics is transaction-level and carries no subscription-specific metrics. It reports orders, products, customers, and revenue, but a renewal order and a new-customer order look identical to it, so "revenue" blends acquisition with retention. MRR, churn, tenure, and lifetime value require modelling the subscription as an entity with a status lifecycle, normalizing each plan's actual recurring total to a month, and excluding trials until they convert. That's a different data structure from an order table, which is why no amount of pixel or tag fixing produces it.
QWhat breaks if I just install more plugins to cover each gap?
You get more numbers that don't reconcile. Each plugin defines "active subscriber," "revenue," and "conversion" its own way, at its own refresh cadence, on its own currency handling — so a Monday review turns into arguing about which dashboard is right instead of what to do. The specific failure to watch for is duplicate conversion events: two tools both sending Purchase server-side, with no shared event_id, doubles your reported conversions and halves your apparent cost per acquisition. Before adding anything, inventory what's already firing, and confirm each conversion has exactly one emitter.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.