How to attribute Lemon Squeezy revenue to the channel that earned it

Tag the Lemon Squeezy checkout with a visitor ID, read it from the order_created webhook, and credit the pre-tax total. 5 steps with code and GA4 limits.

Muzahid Maruf — Founder of TrackRev.io

Muzahid MarufUpdated

Revenue attribution · 9 min read
On this page
  1. 01Why does a Lemon Squeezy order carry no channel?
  2. 02How does Lemon Squeezy revenue attribution work in 5 steps?
  3. 03What can Google Analytics 4 show for Lemon Squeezy sales?
  4. 04How does TrackRev read Lemon Squeezy orders?
  5. 05What changes when Lemon Squeezy users move to Stripe's merchant of record product?
  6. 06Which limits and fixed values should you know?

Explore with AI

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

Lemon Squeezy revenue attribution is the practice of attaching a channel to the checkout before the buyer pays and reading the channel back from the webhook after.

A Lemon Squeezy order holds the buyer's email, amounts and tax but no UTM, referrer or source field, so an untagged sale looks the same whether a newsletter or an ad sent the buyer.

The documented route is checkout[custom], returned as meta.custom_data on webhooks, and the route takes 5 steps to turn into revenue per channel.

Key takeaways

  • A Lemon Squeezy order has no UTM, referrer or source field, so a channel reaches the order only through checkout[custom].
  • Lemon Squeezy returns custom data in meta.custom_data on 3 webhook object types and omits it from the orders API, so a missed webhook costs you the tag.
  • The Lemon Squeezy total includes tax: its sample order has a subtotal of 999, tax of 200 and a total of 1199 cents, so credit total minus tax.
  • Count a Lemon Squeezy subscription's first payment once, from the order, and take later payments from renewal invoices keyed by subscription ID.
  • TrackRev matches Lemon Squeezy orders to visits by buyer email and needs a paid plan from $29 a month.

Why does a Lemon Squeezy order carry no channel?

A Lemon Squeezy order is a record of the buyer and the amounts, not of the channel, because the checkout is hosted on a store subdomain of lemonsqueezy.com, so your cookies never arrive and the UTM parameters on your page stay there unless you copy them into the buy link.

The hosted checkout docs also suggest pasting the buy link straight into emails, newsletters and social posts, where no page script can add tags.

The order object then records the buyer, currency, amounts, tax, status and first order item, with no UTM, referrer or source attribute.

Its only attribution fields, affiliate_id and referral_amount, belong to Lemon Squeezy's own affiliate program and are null on every other sale.

How does Lemon Squeezy revenue attribution work in 5 steps?

Lemon Squeezy revenue attribution runs in 5 steps. Steps 1 and 2 run in the browser, step 3 on your server, and steps 4 and 5 set the amount each channel gets.

Step 1: How do you save the source and the buyer's identity?

Saving the source means reading the UTM parameters on the first pageview, storing them against a visitor ID on your server, and adding the buyer's email to the ID at signup.

WebKit's tracking prevention policy deletes script-written cookies and storage after 7 days without a visit and caps script-written cookies at 24 hours when WebKit detects link decoration, so a buyer who stays away for 2 weeks between the first visit and checkout can arrive with nothing saved.

A server-side record survives the gap. For example, a visitor who reads a newsletter link on day 1 and buys on day 14 still carries a stored source if the ID sits in your database.

The Safari ITP post has more.

Step 2: How do you tag the checkout?

Tagging the checkout means adding custom values to the buy link or the API request. Lemon Squeezy's custom data guide accepts any number of checkout[custom] fields on the checkout link and asks you to keep them minimal.

Send 2 values: a visitor ID, which resolves to the full history in your own store, and a short channel label such as newsletter, which keeps the sale readable if the lookup fails. Add checkout[email] when you know the address.

The prefill parameters also take name, billing country, tax number and discount code. Paddle Billing uses the same pattern.

Checkout routeWhere the tag goesCheck before you rely on it
Hosted linkcheckout[custom][vid]=... on the /checkout/buy linkThe value arrives as a string
Overlay buttonThe same parameters on the link with the lemonsqueezy-button class1 test-mode purchase to confirm
API checkoutcheckout_data.custom in the POST /v1/checkouts body, then open the returned urlLemonSqueezy.Url.Open takes a URL string only

From Lemon Squeezy's custom data guide and Lemon.js methods docs.

Browser: a tagged hosted checkout link
const url = new URL("https://yourstore.lemonsqueezy.com/checkout/buy/VARIANT_ID");
url.searchParams.set("checkout[custom][vid]", visitorId);
url.searchParams.set("checkout[custom][src]", firstSource); // for example "newsletter"
if (user?.email) url.searchParams.set("checkout[email]", user.email);
buyButton.href = url.toString();
Server: the same tag on a checkout created through the API
const res = await fetch("https://api.lemonsqueezy.com/v1/checkouts", {  method: "POST",  headers: {    Accept: "application/vnd.api+json",    "Content-Type": "application/vnd.api+json",    Authorization: "Bearer " + process.env.LS_API_KEY,  },  body: JSON.stringify({    data: {      type: "checkouts",      attributes: { checkout_data: { email: user.email, custom: { vid: visitorId, src: firstSource } } },      relationships: {        store: { data: { type: "stores", id: STORE_ID } },        variant: { data: { type: "variants", id: VARIANT_ID } },      },    },  }),});const checkoutUrl = (await res.json()).data.attributes.url;

Step 3: Which webhook events carry the channel?

5 webhook events are the ones that matter for attribution. Lemon Squeezy lists 17 webhook events and suggests subscribing to at least 4. The table shows the object each event sends and what to store.

Subscribe to all 5, because a renewal invoice names the subscription but not the original channel, and a refund event reverses a sale or a renewal.

EventObject sentWhat to store
order_createdOrderThe sale, its amounts and meta.custom_data
subscription_createdSubscriptionThe channel, keyed by subscription ID
subscription_payment_successSubscription invoiceEach renewal, looked up by subscription_id
order_refundedOrderA full or partial refund
subscription_payment_refundedSubscription invoiceA refunded renewal

From Lemon Squeezy's webhook event types.

How do you verify and store the webhook?

Verifying a webhook means checking the signature on the raw body before trusting any field.

Lemon Squeezy signs each delivery with an HMAC hex digest of the raw body in an X-Signature header, using a secret you choose, as its signing guide shows. Verify the digest, then answer 200.

Any other status gets up to 3 more attempts, after waits such as 5, 25 and 125 seconds, before the delivery counts as failed, per the webhook request docs. Custom values arrive as strings, so 123 becomes "123".

Skip orders flagged test_mode, key each stored sale on its Lemon Squeezy ID so a retry cannot count twice, and when the custom data is empty, match user_email against your step 1 records.

Webhook endpoint (Next.js route handler)
import crypto from "node:crypto"; export async function POST(req: Request) {  const raw = await req.text();  const sent = Buffer.from(req.headers.get("x-signature") ?? "");  const want = Buffer.from(    crypto.createHmac("sha256", process.env.LS_SIGNING_SECRET!).update(raw).digest("hex"),  );  if (sent.length !== want.length || !crypto.timingSafeEqual(sent, want)) {    return new Response("bad signature", { status: 401 });  }   const event = JSON.parse(raw);  const name = event.meta.event_name;  const a = event.data.attributes;  const tag = event.meta.custom_data ?? {}; // strings; empty when the checkout was untagged  if (a.test_mode) return new Response("ok");   if (name === "order_created") {    await saveSale({      key: "order_" + event.data.id,      email: a.user_email,      cents: a.total - a.tax, // total includes tax      currency: a.currency,      vid: tag.vid,      src: tag.src,    });  } else if (name === "subscription_created") {    await saveChannel(String(event.data.id), { vid: tag.vid, src: tag.src });  } else if (name === "subscription_payment_success" && a.billing_reason === "renewal") {    const channel = await channelFor(String(a.subscription_id));    await saveSale({      key: "invoice_" + event.data.id,      email: a.user_email,      cents: a.total - a.tax,      currency: a.currency,      ...channel,    });  }  return new Response("ok");}

Step 4: Why credit the pre-tax amount?

The pre-tax amount is the revenue a channel earned, and the Lemon Squeezy total includes tax. Amounts are integers in cents, so credit the total minus the tax.

In the sample that is 1199 minus 200, or 999, and total_usd equals total at a currency_rate of 1.0000.

On a $19 plan, crediting the total overstates revenue by the buyer's tax rate, and the error compounds across a year: a 19% tax adds $3.61 to each payment and $43.32 to 12 payments, and at 20% tax, 12 months at the total comes to $273.60 against $228.00 of true revenue.

refunded turns true only for a full refund, so subtract refunded_amount to handle partial ones. The sample is tax-exclusive, so spot-check 1 order where tax_inclusive is true.

The pricing page lists a 5% + 50¢ fee per transaction the payload omits, so the figure is revenue before tax and fees.

FieldSample valueUse it for
subtotal999The sample's price before tax
tax200Subtract it from the total
total1199What the buyer paid, tax included
total_usd1199The same total in dollars
currency_rate1.0000The USD rate at purchase

From the sample order in Lemon Squeezy's webhook request docs.

Tax rateTaxTotalTotal in dollarsCredit12 months at the totalTax over 12 monthsOverstated
0%01900$19.00$19.00$228.00$0.000%
19%3612261$22.61$19.00$271.32$43.3219%
20%3802280$22.80$19.00$273.60$45.6020%
25%4752375$23.75$19.00$285.00$57.0025%

One $19 plan priced before tax, in cents, at 4 example tax rates.

Step 5: How do you carry the channel to every renewal?

Carrying the channel to every renewal means saving the channel against the subscription ID once and looking the channel up on each renewal.

Lemon Squeezy documents custom data for order, subscription and license key events and names no invoice events, so save the channel when the subscription is created.

A $19 plan held for 12 months pays $228 across 12 payments, 1 from the order and 11 from renewal invoices, so crediting only the first payment gives the channel $19, or 8.3%, and hides which channel pays your MRR.

First, credit the first payment once, from the order. Second, credit each renewal from its invoice. Finally, skip failed payments. The subscription LTV attribution post covers reporting.

MomentEvents Lemon Squeezy sendsCredit
Month 1, signuporder_created, subscription_created, subscription_payment_success (initial)$19.00 from the order
Months 2 to 12, each renewalsubscription_payment_success (renewal), subscription_updated$19.00 from each invoice
Month 3, card declinedsubscription_payment_failed, subscription_updated$0.00
Month 3, a retry workssubscription_payment_success (renewal), subscription_payment_recovered, subscription_updated$19.00 from the success invoice

One $19 monthly subscription, from Lemon Squeezy's event flow examples. The first payment appears as an order and as an initial invoice, so credit it once. A failed renewal gets 4 retries over 2 weeks.

What can Google Analytics 4 show for Lemon Squeezy sales?

GA4 is the right tool for browser behavior on Lemon Squeezy sales, and the webhook is the right source for revenue totals.

Lemon Squeezy's Google Analytics integration takes a Measurement ID in the store's Design settings and tracks 8 storefront and checkout events, including purchase.

The overlay needs the Google tag on your own site, and the docs warn that ad blockers can stop the tag. GA4 can join a purchase to the visit that began on your site through cross-domain measurement.

Per Google's cross-domain guide, GA4 passes IDs in a _gl URL parameter and needs the same G- tag ID on every domain, or a visitor crossing root domains counts as 2 users and 2 sessions.

Add your store domain under Configure your domains, run 1 test-mode purchase and check DebugView, since neither vendor documents the pairing.

How do you remove the lemonsqueezy.com referral from GA4?

A lemonsqueezy.com referral is the label GA4 shows when buyers click back from the confirmation modal, and the label hides the real channel of the visit.

Google's list of unwanted referrals takes up to 50 domains per data stream and sets ignore_referrer to true. The list clears the label; carrying the visitor across domains takes cross-domain measurement.

Listing lemonsqueezy.com uses 1 of the 50 domain slots in the stream. The GA4 revenue by channel post covers the reports.

How does TrackRev read Lemon Squeezy orders?

TrackRev is SaaS affiliate software with revenue attribution built in, and we built TrackRev to read Lemon Squeezy through the orders API.

TrackRev connects with a Lemon Squeezy API key and an optional store ID, polls orders on a schedule, and syncs at once when a signed webhook arrives.

Because the orders API carries no custom data, TrackRev matches each order to a visit by the buyer's email.

In our pixel, window.trk.identify(email) runs at signup, and the order is credited under your workspace's model of last-touch, first-touch or linear (how they differ). The pixel also adds checkout[custom][vid] to Lemon Squeezy buy links.

The value reaches your endpoint from step 3, but our own Lemon Squeezy attribution ignores it, so a buyer who pays with a different email than the one you identified goes unattributed.

What does the TrackRev Lemon Squeezy connection not read?

In our connection today, the sync reads orders, which cover one-time purchases and each subscription's first payment. The sync does not read renewal invoices or discount codes, so renewal revenue and promo-code attribution are missing for Lemon Squeezy stores.

A fully refunded order flips to refunded on a later sync and its partner commissions are reversed.

Revenue figures need a paid plan: Indie at $29 a month covers 1 workspace and Growth at $299 a year covers up to 10 workspaces.

The free plan covers link tracking only, with 50 links and 1,000 tracked events a month. Connection steps are on the Lemon Squeezy integration page and plans are on pricing.

With a single product, a channel or two and a webhook endpoint already running, the handler in step 3 is the whole job.

What changes when Lemon Squeezy users move to Stripe's merchant of record product?

Moving to Stripe Managed Payments changes the source of checkout and webhooks. On January 28, 2026, Lemon Squeezy's CEO wrote that Stripe, which acquired Lemon Squeezy in 2024, is building Stripe Managed Payments as its merchant of record product.

It then supported merchants in 35+ countries, and the stated aim was an easy migration path for Lemon Squeezy users.

After a move, checkout and webhooks would come from Stripe, so the tagging layer above would need rebuilding against Stripe's objects.

The Stripe metadata attribution post covers the Stripe version of this pattern, and Stripe's docs should confirm how Managed Payments exposes metadata first.

Which limits and fixed values should you know?

ItemValueSource
Webhook signing secretTypically 6 to 40 charactersLemon Squeezy signing guide
Redelivery after a non-200 reply3 attempts, after 5, 25 and 125 secondsLemon Squeezy webhook requests
Object types whose webhooks carry custom data3: order, subscription, license keyLemon Squeezy custom data guide
Webhook events listed17Lemon Squeezy event types
Webhook events suggested at minimum4Lemon Squeezy event types
Order statuses6Lemon Squeezy order object
Invoice billing reasons3: initial, renewal, updatedLemon Squeezy invoice object
Checkout events sent to Google Analytics8Lemon Squeezy analytics docs
Failed renewal retries4 over 2 weeksLemon Squeezy subscription object
Script-written storage in SafariDeleted after 7 days without a visitWebKit tracking prevention
Script-written cookie after link decorationCapped at 24 hoursWebKit tracking prevention
Unwanted referral domains50 per data streamGoogle Analytics help
Cross-domain conditions100 per streamGoogle Analytics help
Lemon Squeezy fee5% + 50¢ per transactionLemon Squeezy pricing
Countries Stripe Managed Payments supported on January 28, 202635+Lemon Squeezy 2026 update
TrackRev Indie plan$29 a monthTrackRev pricing
TrackRev free plan50 links and 1,000 events a monthTrackRev pricing

Values as published in October 2026.

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 founded TrackRev.io, SaaS affiliate software with no limit on tracked revenue, and Contant.io. He writes about affiliate programs.

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

Run your affiliate program without a revenue cap.

Affiliate software for indie hackers and small SaaS teams: $29 a month flat, 0% fees, link tracking and revenue attribution built in. Free to start, no card.