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, FounderUpdated
On this page
- 01Why does a Lemon Squeezy order carry no channel?
- 02How does Lemon Squeezy revenue attribution work in 5 steps?
- 03What can Google Analytics 4 show for Lemon Squeezy sales?
- 04How does TrackRev read Lemon Squeezy orders?
- 05What changes when Lemon Squeezy users move to Stripe's merchant of record product?
- 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 route | Where the tag goes | Check before you rely on it |
|---|---|---|
| Hosted link | checkout[custom][vid]=... on the /checkout/buy link | The value arrives as a string |
| Overlay button | The same parameters on the link with the lemonsqueezy-button class | 1 test-mode purchase to confirm |
| API checkout | checkout_data.custom in the POST /v1/checkouts body, then open the returned url | LemonSqueezy.Url.Open takes a URL string only |
From Lemon Squeezy's custom data guide and Lemon.js methods docs.
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();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.
| Event | Object sent | What to store |
|---|---|---|
| order_created | Order | The sale, its amounts and meta.custom_data |
| subscription_created | Subscription | The channel, keyed by subscription ID |
| subscription_payment_success | Subscription invoice | Each renewal, looked up by subscription_id |
| order_refunded | Order | A full or partial refund |
| subscription_payment_refunded | Subscription invoice | A 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.
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.
| Field | Sample value | Use it for |
|---|---|---|
| subtotal | 999 | The sample's price before tax |
| tax | 200 | Subtract it from the total |
| total | 1199 | What the buyer paid, tax included |
| total_usd | 1199 | The same total in dollars |
| currency_rate | 1.0000 | The USD rate at purchase |
From the sample order in Lemon Squeezy's webhook request docs.
| Tax rate | Tax | Total | Total in dollars | Credit | 12 months at the total | Tax over 12 months | Overstated |
|---|---|---|---|---|---|---|---|
| 0% | 0 | 1900 | $19.00 | $19.00 | $228.00 | $0.00 | 0% |
| 19% | 361 | 2261 | $22.61 | $19.00 | $271.32 | $43.32 | 19% |
| 20% | 380 | 2280 | $22.80 | $19.00 | $273.60 | $45.60 | 20% |
| 25% | 475 | 2375 | $23.75 | $19.00 | $285.00 | $57.00 | 25% |
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.
| Moment | Events Lemon Squeezy sends | Credit |
|---|---|---|
| Month 1, signup | order_created, subscription_created, subscription_payment_success (initial) | $19.00 from the order |
| Months 2 to 12, each renewal | subscription_payment_success (renewal), subscription_updated | $19.00 from each invoice |
| Month 3, card declined | subscription_payment_failed, subscription_updated | $0.00 |
| Month 3, a retry works | subscription_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?
| Item | Value | Source |
|---|---|---|
| Webhook signing secret | Typically 6 to 40 characters | Lemon Squeezy signing guide |
| Redelivery after a non-200 reply | 3 attempts, after 5, 25 and 125 seconds | Lemon Squeezy webhook requests |
| Object types whose webhooks carry custom data | 3: order, subscription, license key | Lemon Squeezy custom data guide |
| Webhook events listed | 17 | Lemon Squeezy event types |
| Webhook events suggested at minimum | 4 | Lemon Squeezy event types |
| Order statuses | 6 | Lemon Squeezy order object |
| Invoice billing reasons | 3: initial, renewal, updated | Lemon Squeezy invoice object |
| Checkout events sent to Google Analytics | 8 | Lemon Squeezy analytics docs |
| Failed renewal retries | 4 over 2 weeks | Lemon Squeezy subscription object |
| Script-written storage in Safari | Deleted after 7 days without a visit | WebKit tracking prevention |
| Script-written cookie after link decoration | Capped at 24 hours | WebKit tracking prevention |
| Unwanted referral domains | 50 per data stream | Google Analytics help |
| Cross-domain conditions | 100 per stream | Google Analytics help |
| Lemon Squeezy fee | 5% + 50¢ per transaction | Lemon Squeezy pricing |
| Countries Stripe Managed Payments supported on January 28, 2026 | 35+ | Lemon Squeezy 2026 update |
| TrackRev Indie plan | $29 a month | TrackRev pricing |
| TrackRev free plan | 50 links and 1,000 events a month | TrackRev pricing |
Values as published in October 2026.
Found this useful? Share it.
Frequently asked questions
- No. The order object has no UTM, referrer or source attribute, so your own data reaches an order only through checkout[custom].
- Append checkout[custom][utm_source]=newsletter to the checkout link, or send checkout_data.custom through the API. One visitor ID resolved on your server is shorter than 3 raw UTM strings.
- In meta.custom_data on Lemon Squeezy webhooks for 3 object types: orders, subscriptions and license keys. Every value is a string, and the orders API does not return it.
- No. In Lemon Squeezy's sample order the total of 1199 is the subtotal of 999 plus tax of 200, so subtract the tax field to get the pre-tax amount.
- Yes, if each link carries its own channel, such as checkout[custom][src]=newsletter in 1 link and checkout[custom][src]=youtube in another. No page view means no visitor ID.
- Use test mode: open a test checkout link with your tag, pay with a Lemon Squeezy test card, then read meta.custom_data in the webhook log.
- No. The free plan covers 50 links and 1,000 tracked events a month but hides revenue figures. Revenue attribution starts on the Indie plan at $29 a month.

Written by
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.