TrackRev
Blog
7 min read
Paddle revenue attribution

Paddle Revenue Attribution by Marketing Channel

Paddle's cross-domain checkout breaks cookie tracking, hiding your MRR by channel. Use its passthrough parameter for channel-level revenue attribution.

Muzahid Maruf — Founder of TrackRev.io

Muzahid Maruf, Founder

LinkedIn

On this page
  1. 01The Paddle attribution gap
  2. 02Paddle's passthrough mechanism
  3. 03The three-step setup
  4. 04Passing customData into Paddle Checkout
  5. 05Reading customData from the Paddle webhook
  6. 06Common mistakes in Paddle attribution setups
  7. 07Paddle vs Lemon Squeezy vs Stripe attribution
  8. 08What channel revenue looks like for Paddle products
  9. 09TrackRev and Paddle attribution

Explore with AI

Opens this article inside the chosen assistant with a ready-made prompt.

To see which acquisition channel actually pays your Paddle MRR, you have to carry a session ID across the checkout boundary — because Paddle's checkout happens on checkout.paddle.com, a different domain from your site, which breaks standard cookie-based channel attribution for 100% of Paddle transactions unless you use the passthrough parameter.

Paddle attribution works similarly to Lemon Squeezy: capture a session ID on your landing page, pass it through Paddle Checkout via the customData (or legacy passthrough) field, then read it back from the transaction.completed webhook to match the charge to the originating click.

Paddle handles the tax/MoR side, but the attribution side is on you. This guide walks the setup, the webhook events that matter, and how Paddle compares to Stripe and Lemon Squeezy on attribution complexity.

Key Takeaways

  • Paddle's checkout on checkout.paddle.com breaks cookie tracking for 100% of transactions — so without the passthrough handshake you cannot see which channel pays your MRR; first-party cookies are scoped to the origin that set them, and no browser setting changes this.
  • Use customData (Paddle Billing / v2) or passthrough (Paddle Classic) to carry the visitor ID through checkout — mixing them up is the most common silent attribution failure.
  • Always attribute on transaction.completed — not payment.created (fires before charge succeeds) or subscription.created (misses all subsequent renewals).
  • The customData you set on the first checkout sticks to the subscription for its entire lifetime, so every renewal auto-attributes to the original channel without any additional code.
  • Keep the passthrough payload flat and small (visitor ID + up to 3 UTM keys) — deep nesting causes parse mismatches on Paddle Classic and can silently truncate above ~1KB.

The Paddle attribution gap

Paddle is the Merchant of Record for many SaaS products — it handles tax, compliance, refunds, and global payment methods on your behalf for around 5% + 50¢ per transaction, materially more than a pure payment processor like Stripe (2.9% + 30¢, where tax handling stays your responsibility).

The trade-off for that all-in fee is that checkout runs on checkout.paddle.com (or a Paddle-managed domain), which is a different domain from your marketing site. First-party cookies set on yourbrand.com do not follow the visitor into Paddle's checkout.

The exact mechanics of the gap: click lands on your page → first-party cookie is set with the session ID → visitor clicks "Buy Now" → browser redirects to checkout.paddle.com → your cookie is left behind on the original domain → Paddle processes payment → the webhook fires with no attribution data attached.

The Paddle dashboard then shows transactions, customers, and revenue — but not which marketing channel drove each sale, because that information never made the cross-domain trip.

Definition: Paddle's passthrough (or customData in Paddle Billing) is a string or object parameter you can attach to any checkout session that Paddle will return unchanged in the webhook payload.

It is the supported bridge for attribution — the way you carry the session ID across the domain boundary without depending on cookie state at checkout completion time.

The cross-domain redirect lifecycle

Every Paddle purchase walks through the same six-stage lifecycle: marketing click → landing on your domain → first-party cookie set with the session ID → "Buy now" tap → 302 redirect to checkout.paddle.com → payment completion and webhook fire.

The cookie set in stage three is bound to your origin, so it is no longer accessible the moment the browser hits stage five.

Any attribution decision that depends on reading that cookie after the redirect is structurally impossible — the data is not there.

The whole reason the passthrough parameter exists is to carry the session ID through the gap created by stages four and five.

Why standard cookies cannot survive the boundary

First-party cookies are scoped to the origin that set them, not to the user's session — that scoping is a browser security guarantee, not a Paddle quirk.

Setting SameSite=None; Secure does not help, because the cookie is still bound to yourbrand.com rather than checkout.paddle.com. There is no browser configuration, polyfill, or Paddle dashboard toggle that would make a cookie on your domain readable from Paddle's domain.

The passthrough field is the only sanctioned mechanism — anything else either depends on third-party cookies (already blocked by Safari and Firefox) or fingerprinting (legally fraught and unreliable).

Paddle's passthrough mechanism

Paddle has supported a passthrough/customData field for years across both major versions of the platform.

Technically the field is either a URL parameter (when redirecting to a hosted checkout link) or a JS API parameter (when using Paddle.Checkout.open inline), accepts up to ~1,000 characters of payload, and returns unchanged in every webhook event for the lifetime of the resulting subscription or transaction.

Which field name to use depends on your Paddle account version. If you created your Paddle account after October 2022, you are on Paddle Billing (the v2 API) and should use customData — an arbitrary JSON object.

Older accounts are on Paddle Classic and use passthrough — a string, typically a JSON-encoded blob you parse on the webhook side.

Mixing the two is the single most common cause of broken Paddle attribution: passthrough on a Billing account is silently ignored, and customData on a Classic account simply doesn't exist.

Use it to carry the visitor ID and any UTM context you want preserved across the cross-domain checkout boundary.

A typical payload looks like { vid: "abc123", utm_source: "newsletter", utm_campaign: "q2-launch" } on Billing, or the same data JSON-encoded into a string on Classic. For a field-by-field walkthrough, see our guide to using passthrough and custom_data to track every Paddle sale.

Setting passthrough on a Paddle Classic checkout URL
const passthrough = JSON.stringify({  vid: visitorId,  utm_source: utmSource,});const checkoutUrl =  `https://buy.paddle.com/product/PRODUCT_ID?passthrough=${encodeURIComponent(passthrough)}`;

Paddle Billing vs Paddle Classic field naming

The Billing API takes a typed JSON object on customData while Classic takes an opaque string on passthrough.

The practical implication is that on Billing you write customData: { vid } and the webhook receives a parsed object; on Classic you write passthrough: JSON.stringify({ vid }) and the webhook receives a string you have to JSON.parse yourself.

Picking the wrong field name on the wrong account version is the most common silent failure: Billing ignores passthrough entirely, and Classic does not recognise customData as a parameter. Always check your account creation date before writing the integration.

Payload structure for visitor + UTM data

Keep the passthrough payload small and flat: a session ID, plus the two or three UTM keys that matter (utm_source, utm_campaign, utm_medium).

Avoid nesting because Classic's string serialization makes nested objects fragile, and Billing's object schema does not gain anything from depth.

Treat the field as a routing key, not a full event payload — the real attribution data lives in your own database, joined by the visitor ID.

A 100-byte payload is plenty for attribution; anything larger is usually a sign the wrong data is being threaded through checkout.

The three-step setup

Step 1 — Create a tracking link per channel. Each marketing channel (newsletter, YouTube description, partner referral, paid social campaign) gets its own short link with UTM values baked in.

The pixel on your landing page reads those UTM values and writes them into a first-party cookie along with a session ID.

Without this step there is no source value to record — the cookie has nothing to carry — and every conversion shows up as (direct) regardless of where the click actually originated.

Step 2 — Read the session ID on your landing page and pass it to Paddle Checkout as customData. Read the visitor ID from your first-party cookie at checkout time and include it in the Paddle Checkout config.

On Paddle Billing this means setting customData: { vid: cookieValue } in the Paddle.Checkout.open call; on Paddle Classic it means JSON-encoding the data into the passthrough URL parameter on the buy-link.

The crucial detail is reading the cookie at the moment of checkout creation — not earlier, when it might not be set, and not client-side after redirect, when it's already gone.

Step 3 — Read custom_data from the transaction.completed webhook and match to the click record. The webhook fires every time a payment succeeds — first charge, renewals, upgrades — and carries the customData blob through unchanged.

Your handler parses the vid, looks up the originating click in your attribution store, and credits the channel that earned the conversion.

The same handler runs for every recurring charge afterward, so LTV per channel updates automatically without a separate reconciliation step.

The cookie read has to happen at the exact moment you build the Paddle Checkout config — not earlier in the page lifecycle and not after the redirect.

On a single-page app the visitor ID may be unset for the first few hundred milliseconds while the pixel finishes binding, so a value cached in a module-level constant on page load is often empty or stale.

Read document.cookie inside the click handler that opens Paddle.Checkout, immediately before you call Paddle.Checkout.open({ customData }). That guarantees the final settled visitor ID is what gets threaded into the checkout session.

Handling renewals through the same handshake

The customData you set on the first checkout sticks to the resulting subscription for its entire lifetime.

Every renewal fires another transaction.completed webhook with the same custom_data payload attached, which is what lets LTV per channel update automatically without a re-attribution step.

The implication: pick a stable schema upfront, because changing the field shape later means renewals on old subscriptions still carry the old structure.

A flat { vid, utm_source, utm_campaign } object handles both the first charge and the renewals that follow without further work.

Field-length limits on customData

Paddle Billing's customData object accepts up to roughly 4KB of JSON; Paddle Classic's passthrough string caps closer to 1KB after URL encoding.

Stuffing a full UTM dictionary plus referrer plus campaign metadata into the field works for Billing but truncates silently on Classic — and truncated JSON parses as an empty object on your webhook.

Keep the payload to a visitor ID plus at most a handful of UTM keys.

Passing customData into Paddle Checkout

Client: Paddle Checkout with customData
// Read the first-party visitor cookieconst vid = document.cookie  .split("; ")  .find((row) => row.startsWith("vid="))  ?.split("=")[1]; // Open Paddle Checkout with customData attachedPaddle.Checkout.open({  items: [{ priceId: "pri_01h...", quantity: 1 }],  customData: {    vid: vid ?? null,  },  customer: {    email: user.email,  },});

Verifying Paddle's webhook signature

Paddle Billing signs every webhook with an HMAC in the Paddle-Signature header (timestamped, similar to Stripe), and Paddle's signature-verification docs provide both an SDK helper and the manual HMAC steps.

Verify it against your notification secret before parsing the body — an unverified handler will happily attribute attacker-crafted transactions to whatever channel they send.

Paddle Classic uses a legacy public-key signature scheme on the p_signature field; the verification is different but the requirement is the same.

Reading customData from the Paddle webhook

Webhook handler: read customData on transaction.completed
// POST /api/paddle/webhookconst event = JSON.parse(rawBody); if (event.event_type === "transaction.completed") {  const vid = event.data?.custom_data?.vid;  const email = event.data?.customer?.email;  const revenue = parseFloat(event.data?.details?.totals?.total ?? "0") / 100;   attributeTransaction({ vid, email, revenue });}

Common mistakes in Paddle attribution setups

Three mistakes account for nearly every broken Paddle attribution we see when teams reach out for help.

All three have the same surface symptom — conversions appearing as (direct) when they should be attributed — and all three are quick to fix once you know what to look for.

Mistake 1: Not URL-encoding the session ID in the passthrough parameter (Paddle Classic). Because passthrough is a URL query parameter, any unescaped ampersand or special character in the payload truncates the value.

The classic failure: serialising a JSON object with JSON.stringify and dropping it straight into the URL — the inner quotes and braces break the parse on Paddle's side, the field arrives empty, and the webhook fires with no attribution.

Wrap every passthrough payload in encodeURIComponent() on the client and decodeURIComponent() + JSON.parse() on the webhook.

Mistake 2: Using the Paddle Classic passthrough field on a Paddle Billing account. If your account was created after October 2022, you are on Paddle Billing — and Paddle Billing silently ignores passthrough.

The data goes nowhere, the webhook arrives with an empty custom_data, and the only signal anything is wrong is the attribution gap. Use customData (object, not string) on Billing accounts.

If you inherited integration code from a Classic-era Paddle setup, audit the field name first.

Mistake 3: Listening to the wrong webhook event. The correct event for payment completion on Paddle Billing is transaction.completed.

payment.created fires when the payment is initiated, before the charge has actually succeeded — listening for it means you'll attribute conversions that later fail, refund, or get blocked by 3DS. subscription.created fires once per subscription, missing all subsequent renewal revenue.

Always handle transaction.completed as the primary attribution signal.

Detecting silent passthrough truncation

When the passthrough payload exceeds Classic's ~1KB cap, Paddle truncates the string with no error returned at checkout creation time — the truncation only surfaces when your webhook handler tries to JSON.parse the resulting fragment and throws.

Add a length assertion in your client code: if encodeURIComponent(passthrough).length exceeds 950 bytes, drop the lowest-priority keys (typically utm_term or utm_content) before sending.

Logging a warning when the assertion trips gives you advance notice instead of a flood of webhook parse errors in production.

Differentiating transaction.completed from payment.created

payment.created fires at the instant Paddle accepts the payment intent, before the bank has confirmed funds.

A 3DS challenge that the buyer never completes, or a card decline that arrives 200ms later, both leave you with a fired payment.created event but no actual revenue.

transaction.completed only fires once funds are confirmed and the order is in a terminal success state, which makes it the only event safe to use as an attribution writer.

If you absolutely need an earlier signal for funnel metrics, log payment.created to a separate event store — never to your revenue table.

Paddle vs Lemon Squeezy vs Stripe attribution

Across the three checkout providers, the mechanism is the same — pass a session ID through, read it back from the webhook — but the field name differs.

ProviderCustom data fieldWebhook eventRecurring support
Stripemetadatacheckout.session.completed✓ via subscription events
Lemon Squeezycheckout[custom][...]order_created✓ via subscription_created
PaddlecustomDatatransaction.completed✓ via subscription.created
Polarmetadataorder.created✓ via subscription.created

What channel revenue looks like for Paddle products

Based on attribution data across TrackRev workspaces using Paddle, Paddle-hosted products skew toward B2B SaaS in regulated markets where Merchant-of-Record matters. The channel mix reflects that — owned, content, and partner channels dominate, with longer attribution windows than indie products.

ChannelAvg. click-to-trial rateAvg. click-to-paid rateAvg. revenue per 1,000 clicks
Newsletter (owned)3.8%2.9%$185
YouTube organic1.9%1.4%$92
Twitter/X organic0.9%0.6%$38
Affiliate / partner3.2%2.6%$168
Direct5.4%4.1%$261

Source: TrackRev attribution data for Paddle-connected workspaces, 2026.

A few things are worth flagging in the table.

A healthy revenue-per-1,000-clicks figure for B2B Paddle products sits in the $150–$200 range for owned channels; anything materially below that usually signals either an audience-fit problem or a leak in the trial-to-paid conversion step.

Why "Direct" is rarely truly direct on Paddle dashboards

"Direct" is rarely truly direct — it represents dark social (DMs, Slack, Telegram), word of mouth from existing customers, and brand search where the visitor already knew your name.

It consistently produces the highest revenue per click because the buyer is already pre-qualified by the time they land on the checkout page.

On B2B Paddle workspaces the gap is pronounced: direct traffic converts at roughly double the rate of the next-best channel, which is why it dominates the revenue column even when its click volume sits below newsletter and organic search.

Why YouTube outperforms on revenue per click

YouTube is the channel that surprises teams the most — the absolute click volume looks small next to social, but the per-click economics ($92 per 1,000) routinely beat paid channels on the same dashboard once attribution is properly wired up.

The reason is audience intent: a viewer who watches a five-minute demo and then clicks the link in the description is further along the decision curve than someone scrolling past a promoted tweet.

Low volume, high conversion, high revenue per click — a pattern that only becomes visible once attribution is in place.

Cross-currency rounding on MoR payouts

Because Paddle is the Merchant of Record, a transaction billed in EUR or GBP gets converted to your payout currency on settlement — and the FX rate moves between transaction time and webhook time.

Use the details.totals.total in the buyer currency for attribution (it's stable across retries), and reconcile against payout reports in your home currency on a daily batch.

Mixing the two in the same channel report produces ROI numbers that drift 1–3% week-over-week with no underlying change.

Watch out

transaction.created fires before the charge succeeds and subscription.created fires only on the first cycle. Always attribute on transaction.completed — it's the only Paddle event that means money actually moved and that re-fires for every renewal, keeping per-channel LTV accurate. TrackRev's Paddle integration subscribes to the right event by default.

TrackRev and Paddle attribution

TrackRev integrates with both Paddle Billing (v2) and Paddle Classic. Paste your Paddle API key into TrackRev's integration settings, and the customData handshake is wired into the checkout SDK automatically — no manual JavaScript changes.

Related reading: Lemon Squeezy revenue attribution for the parallel LS setup; Stripe revenue attribution for the simpler Stripe-side flow; Polar revenue attribution if you also sell through Polar. TrackRev's free tier covers 1,000 events.

External references: Paddle transactions API; Paddle webhooks documentation; Paddle CAC research.

Once the Paddle customData handshake is wired up, the second decision most SaaS teams face is whether to run a separate affiliate platform alongside it.

The default split — Bitly Growth at $35 a month for link tracking plus Rewardful Starter at $49 a month for affiliates, for $84 a month combined — means two systems consuming the same Paddle transaction.completed webhook with two different definitions of a sale, and a monthly reconciliation to keep the channel and affiliate dashboards in agreement.

TrackRev replaces both for $39 a month on a single Paddle connection with one definition of a sale — 54% cheaper than the two-tool stack and built so affiliate-driven Paddle revenue rolls up in the same view as every other channel.

If you are setting up Paddle attribution today, the same customData passthrough you have just configured is what TrackRev's affiliate program runs on too, with no second integration to maintain.

Paddle attribution and a Paddle affiliate programme are one integration problem, not two — the same customData field that carries a newsletter source to transaction.completed is the field that carries an affiliate ID on the same webhook.

Most SaaS teams run Bitly Growth ($35/mo) for link tracking and Rewardful Starter ($49/mo) for affiliates — $84/mo for two tools with two different definitions of a conversion.

TrackRev is $39/mo for both, on the same Paddle data, with no monthly reconciliation between systems.

Running two tools against one Paddle webhook means writing the same passthrough payload twice, paying two vendors for half a picture each, and reconciling channel and affiliate revenue every month before the dashboards are honest.

The cheaper setup is also the structurally correct one for Paddle's MoR checkout.

Found this useful? Share it.

PostLinkedIn

Frequently asked questions

Muzahid Maruf — Founder of TrackRev.io

Written by

Muzahid Maruf, Founder, TrackRev.io & Contant.io

Muzahid Maruf is the founder of TrackRev.io and Contant.io. He writes about marketing attribution, link tracking, and revenue analytics for SaaS teams.

Writes about Marketing attribution · Link tracking · Revenue analytics · SaaS growth

Keep reading

Related articles from the TrackRev blog.

Stop guessing where your revenue comes from.

Set up TrackRev in 5 minutes. Free tier covers 1,000 events / month — no card needed.

Paddle Revenue Attribution by Marketing Channel · TrackRev