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 · TrackRev.io & Contant.io
On this page
- 01The Paddle attribution gap
- 02Paddle's passthrough mechanism
- 03The three-step setup
- 04Passing customData into Paddle Checkout
- 05Reading customData from the Paddle webhook
- 06Common mistakes in Paddle attribution setups
- 07Paddle vs Lemon Squeezy vs Stripe attribution
- 08What channel revenue looks like for Paddle products
- 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) orpassthrough(Paddle Classic) to carry the visitor ID through checkout — mixing them up is the most common silent attribution failure. - Always attribute on
transaction.completed— notpayment.created(fires before charge succeeds) orsubscription.created(misses all subsequent renewals). - The
customDatayou 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.
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.
Timing the cookie read at checkout creation
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
// 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
// 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.
| Provider | Custom data field | Webhook event | Recurring support |
|---|---|---|---|
| Stripe | metadata | checkout.session.completed | ✓ via subscription events |
| Lemon Squeezy | checkout[custom][...] | order_created | ✓ via subscription_created |
| Paddle | customData | transaction.completed | ✓ via subscription.created |
| Polar | metadata | order.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.
| Channel | Avg. click-to-trial rate | Avg. click-to-paid rate | Avg. revenue per 1,000 clicks |
|---|---|---|---|
| Newsletter (owned) | 3.8% | 2.9% | $185 |
| YouTube organic | 1.9% | 1.4% | $92 |
| Twitter/X organic | 0.9% | 0.6% | $38 |
| Affiliate / partner | 3.2% | 2.6% | $168 |
| Direct | 5.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.
Frequently asked questions
- TrackRev attributes Paddle revenue to the acquisition channel that pays your MRR by carrying a session ID through checkout. Capture a session ID in a first-party cookie when the visitor lands on your site, pass that session ID into Paddle Checkout using the customData field (or the legacy passthrough field on Paddle Classic), then read it back from the transaction.completed webhook and match it to the originating click. Paddle returns the customData unchanged on every event for the life of the subscription, so renewals roll up to the same channel automatically. TrackRev wires this customData handshake into the Paddle checkout SDK for you.
- Paddle does not natively parse or store UTM parameters. You capture the UTM values on your own site at landing time, store them against a session ID in a first-party cookie, and carry that identifier through Paddle Checkout via customData (Paddle Billing) or passthrough (Paddle Classic). The UTM context then comes back in the transaction.completed webhook for attribution.
- Because Paddle is the Merchant of Record, its checkout runs on checkout.paddle.com rather than your own domain. First-party cookies set on your marketing site do not follow the visitor across that domain boundary, so the click context is left behind unless you explicitly pass a session ID into the checkout. The customData (or passthrough) field is the supported bridge that carries the identifier across the cross-domain redirect.
- Yes. TrackRev integrates with both Paddle Billing (v2) and Paddle Classic. After you paste your Paddle API key into TrackRev's integration settings and install the pixel, TrackRev handles the customData passing and webhook matching automatically, so every Paddle transaction is attributed to the channel that drove it alongside your Stripe, Lemon Squeezy, and Polar revenue.
- Accounts created after October 2022 are on Paddle Billing (the v2 API) and use the customData object; older accounts are on Paddle Classic and use the passthrough string. It matters because the field names are not interchangeable — Billing silently ignores passthrough, and Classic does not recognise customData. Check your account creation date before writing the integration, because using the wrong field is the most common cause of a webhook arriving with empty attribution data.
- Paddle charges around 5% + 50¢ per transaction as a Merchant of Record, which bundles global sales tax, VAT, fraud protection, and refunds. Stripe is a payment processor at roughly 2.9% + 30¢ and does not act as Merchant of Record, so you remain responsible for collecting and remitting tax yourself. The higher Paddle fee buys tax compliance you would otherwise build or outsource, which is why many global SaaS teams accept it despite the cross-domain attribution work it adds.

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.
