> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traceten.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Lemon Squeezy

> Attribute Lemon Squeezy revenue to AI-referred sessions.

## What this lets you do

Connect Lemon Squeezy to Traceten and see which AI sources drive paying customers, not just pageviews. When an order is paid, Traceten matches it back to the originating session and attributes the revenue to the correct AI source.

## Before you start

* A Traceten account with at least one site tracked.
* A Lemon Squeezy account, and permission to create API keys on it.
* Your Lemon Squeezy **store ID**. It is the number in your store settings.
* The Traceten snippet installed and verified on the site your checkout runs from.
* Workspace admin in Traceten. Connecting and disconnecting a revenue source affects the whole workspace, so only admins can do it.

## Connect Lemon Squeezy

You connect by pasting an **API key** and your **store ID**. There is no OAuth redirect, and there is no webhook to configure: Traceten creates the webhook on your Lemon Squeezy account for you.

<Steps>
  <Step title="Create an API key in Lemon Squeezy">
    Open [Lemon Squeezy → Settings → API](https://app.lemonsqueezy.com/settings/api) and click **+** to create a key.

    Give it a name (for example, `Traceten`) and copy it. Lemon Squeezy shows the key once.
  </Step>

  <Step title="Find your store ID">
    Open **Settings → Stores** and select the store whose revenue you want to attribute. The store ID is the number shown there.

    Traceten asks for it because a Lemon Squeezy API key covers **every** store on your account. Without the store ID, Traceten cannot tell which store this site sells through.
  </Step>

  <Step title="Paste both into Traceten">
    Go to [app.traceten.com/dashboard](https://app.traceten.com/dashboard), open **Settings → Integrations**, and choose the site your checkout runs from at the top of the page.

    Pick **Lemon Squeezy** from the list of payment providers, then paste the API key and the store ID.

    Click **Connect**.
  </Step>

  <Step title="Confirm the connection">
    Lemon Squeezy shows as **Connected**, with your store name, its currency, and the last four characters of the key.

    Traceten stores the key encrypted. After this point it is never shown again, in the dashboard or in the API, and only those last four characters are readable back.
  </Step>
</Steps>

### What Traceten does with the key

In the same request, Traceten:

1. Reads the store's name and currency, to confirm the key can see the store you named.
2. Reads one order, to prove the key can actually see the revenue it is being connected for.
3. Creates a webhook on your Lemon Squeezy account pointed at Traceten, subscribed to two events.
4. Stores the key and the webhook signing secret, both encrypted.
5. Starts importing your last 90 days of paid orders.

You can see the webhook Traceten created under **Settings → Webhooks**.

These are the two events:

| Event            | What Traceten does with it                                    |
| ---------------- | ------------------------------------------------------------- |
| `order_created`  | Attributes the order. This is the revenue.                    |
| `order_refunded` | Subtracts the refunded amount from the source that earned it. |

**`order_created` carries every payment, renewals included**, because a Lemon Squeezy subscription renewal creates a new order. Every renewal is attributed to the AI source that won the customer, exactly like a first purchase. That is also why Traceten subscribes to neither `subscription_payment_success` nor any `subscription_*` event: the money is already on the order, and taking it from both places would count it twice.

One limit worth knowing: a Lemon Squeezy order does not say which subscription it belongs to, so Lemon Squeezy renewals appear in your revenue totals and per-source breakdowns but not in the per-subscription [lifetime value report](/api/revenue).

### The signing secret

Traceten generates the webhook signing secret and sends it to Lemon Squeezy when it creates the webhook. Lemon Squeezy never returns a webhook secret through its API, so the copy Traceten stores is the only one it will ever have. If that store fails for any reason, Traceten deletes the webhook it just created rather than leaving one behind that it could never verify.

If you rotate the secret by hand in the Lemon Squeezy dashboard, Traceten stops accepting deliveries. Reconnect from the Traceten dashboard to provision a fresh one.

### Keys Traceten will not accept

* **Stripe keys.** Pasting one names the mistake rather than failing on a network call.
* **A key with no store ID.** The connect form asks for both, and refuses with an explanation rather than guessing at a store.
* **A key with whitespace or a line break in it.** Copy it again from Settings → API.

## How attribution works

When an order is paid, Lemon Squeezy sends an `order_created` webhook to Traceten. Traceten then tries to tie the payment back to the originating session, in priority order:

1. **Checkout custom data:** if you passed the visitor's `_traceten_vid` cookie as `traceten_visitor_id`, Traceten matches it to the originating session exactly.
2. **Confirmation modal button:** if you added `?order_identifier=[order_identifier]` to the confirmation modal's button link and the buyer clicks it, the Traceten snippet on that page reports the order identifier and Traceten matches it to the browser that completed the purchase. See below.
3. **Email hash:** if the buyer's session called `traceten.identify({ email })`, Traceten matches the order's email (hashed, never stored) to that visitor.
4. **No match:** the payment is still recorded so revenue totals stay complete, but it shows as **unattributed** (no AI source).

Orders with a status of `pending`, `failed` or `refunded` are not counted. A fully refunded order is skipped outright rather than counted and then taken back off, because there would be nothing to take it off with: the 90-day history import reads orders and never refunds, so importing one that was already refunded would add revenue that no later event ever removes. Only `paid` and `partial_refund` orders produce revenue.

An order that was `paid` when Traceten recorded it and is refunded afterwards is a different case, and that one is reduced: see below.

### Refunds and net revenue

Your attributed revenue is **net of refunds**. When an `order_refunded` event arrives, Traceten subtracts the refunded amount from the AI source the original order was attributed to.

* **The refund is dated on the day it was issued**, not the day of the original order. A past day's revenue figure never changes after the fact, and a day on which you refunded more than you took in reads as a negative number.
* **Conversion counts stay gross.** A refunded order still counts as a conversion. The money is reported separately as a **refunded** figure beside the net one.
* **A partial refund subtracts only the part you returned.** Lemon Squeezy reports `refunded_amount` as the running total refunded against the order, so a second partial refund subtracts only the difference rather than the whole new total. A `partial_refund` order keeps the share you kept.
* **A cancelled subscription is not a refund.** Cancelling stops future charges. The charges already paid were earned and stay in your totals, so cancelling a subscription changes no figure in Traceten. Only an `order_refunded` takes money back off.

Traceten also ignores a delivery for an order created more than seven days ago. Lemon Squeezy's signature carries no timestamp, so this is what stops an old delivery being replayed against your account; Lemon Squeezy's own retries all happen well inside it.

## Checkout overlay: attribute without writing code

A bare Lemon Squeezy checkout link or Buy Now button never goes through your server, so there is no place to attach custom data. Instead, have Lemon Squeezy hand the order identifier back to your own site, and let the snippet do the rest.

<Steps>
  <Step title="Connect Lemon Squeezy to Traceten">
    Follow the connection steps above. Traceten ignores a confirmation-page report for a site whose Lemon Squeezy store is not connected.
  </Step>

  <Step title="Open the product's confirmation modal settings">
    Lemon Squeezy dashboard, **Products → your product → Edit → Confirmation modal**.
  </Step>

  <Step title="Set the button link">
    Point it at a page on your site that has the Traceten snippet installed, and append the `order_identifier` parameter:

    ```
    https://yourdomain.com/thanks?order_identifier=[order_identifier]
    ```

    The **square brackets are part of the syntax** here, not a placeholder to fill in. Lemon Squeezy replaces `[order_identifier]` with the order's unique identifier when it builds the button. Curly braces will not work.

    Use `[order_identifier]`, not `[order_id]`. Traceten does not accept the numeric order id, because it counts up one order at a time and anyone could guess it.
  </Step>

  <Step title="Save, then buy something through the checkout">
    Complete a real purchase and click the button in the confirmation modal. The snippet reads `order_identifier` out of the URL, and the order appears against its AI source in **Revenue**, usually within a minute or two.
  </Step>
</Steps>

<Warning>
  **This is a button the buyer has to click, not an automatic redirect.** Lemon Squeezy's own docs
  say not to rely on the confirmation modal for order fulfilment for exactly this reason. Buyers who
  close the modal are never matched, so this path attributes a meaningful share of your Payment Link
  sales, not all of them. Checkout custom data, where you can use it, matches every order.
</Warning>

More things worth knowing:

* **You change nothing in your code.** The only edit is the button link in Lemon Squeezy.
* **The order of events does not matter.** Lemon Squeezy's webhook usually reaches Traceten before the buyer clicks through. When that happens the order is recorded first and re-attributed the moment the browser reports in, on the original order's own date.
* **The buyer has to click through within 30 minutes of paying.** Traceten pairs a report with an order only when the two are no more than 30 minutes apart. A button clicked later than that is not matched, and the order stays as it was.
* **The page needs the snippet.** If the button points somewhere the snippet is not installed, nothing is reported and the order stays unattributed.
* **Connecting comes first.** Reports that arrive before your store is connected are discarded, not held until later. A report can also be discarded for up to a minute after you connect.

This path matches a browser, not a person. A confirmation link opened on a different device, or forwarded to somebody else, is not matched.

## Passing the visitor ID through checkout

This is the most reliable attribution path, and the only one that works for a buyer who never identified themselves on your site. Pass the visitor's pseudonymous Traceten identifier into the checkout as custom data.

<Tip>
  In the dashboard, open **Settings → Integrations**, pick **Lemon Squeezy** and click **Set up with
  AI**. It copies a prompt for your coding assistant that wires the visitor ID into your checkout.
  The same card shows this site's **Visitor cookie name**.
</Tip>

The robust way to get the value is `window.traceten.getVisitorId()`, called client-side and forwarded to whatever creates the checkout:

```typescript theme={null}
// Client-side: read the visitor id and send it to your own backend
const visitorId = window.traceten.getVisitorId(); // null before the first pageview resolves
```

If you'd rather not change your client-side request shape, read the cookie directly server-side instead:

<Warning>
  Do not hardcode this cookie name. It is per-site and only correct on your configured domain (not
  on localhost). Find the real one under **Sites → Settings → Cookies → Visitor cookie
  name**.
</Warning>

```typescript theme={null}
// Server-side: create a Lemon Squeezy checkout
const visitorId = req.cookies["_traceten_vid"] ?? "";

await fetch("https://api.lemonsqueezy.com/v1/checkouts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LEMONSQUEEZY_API_KEY}`,
    "Content-Type": "application/vnd.api+json",
  },
  body: JSON.stringify({
    data: {
      type: "checkouts",
      attributes: {
        checkout_data: {
          custom: { traceten_visitor_id: visitorId },
        },
      },
      // ... your store and variant relationships
    },
  }),
});
```

Lemon Squeezy returns the value as `meta.custom_data` on the webhook, and Traceten matches it back to the AI session that produced the visit. No personal data leaves your server on this path: the cookie value is pseudonymous and is not PII.

If you use a hosted Lemon Squeezy checkout link instead, append the value as a query parameter:

```
https://yourstore.lemonsqueezy.com/checkout/buy/VARIANT?checkout[custom][traceten_visitor_id]=VISITOR_ID
```

## What the 90-day import can and cannot match

Traceten imports your last 90 days of paid orders when you connect, so revenue from before the webhook existed is not lost. It reads newest first and stops paging at the first order older than 90 days. Lemon Squeezy's orders endpoint has no date filter, so one page (the one straddling that boundary) is retrieved and parsed in full, including the buyer email addresses on the orders that turn out to be older than 90 days. Those orders are discarded in memory: not imported, not stored, not logged. No page beyond the boundary is requested.

Those historical orders match **by email only**. Lemon Squeezy's orders API does not return checkout custom data, so the visitor ID path above applies to live orders only. An imported order whose buyer never called `traceten.identify({ email })` lands as unattributed.

## Verify it worked

1. Lemon Squeezy reads **Connected** in the dashboard.
2. The import banner finishes, and the **Revenue** page shows historical orders.
3. Place a real order (or a test-mode one in a non-production Traceten environment). It appears in **Revenue** within a minute or two.

## Troubleshooting

**"Lemon Squeezy would not let this key read the store's orders."** The key cannot see the store you named. Confirm the store ID belongs to the same account as the key, and create the key with full access.

**"That store ID does not look right."** Traceten accepts only letters, digits, hyphens and underscores in a store ID, because the value goes into a URL. Copy the number from **Settings → Stores**.

**Connected, but revenue is not appearing.** Check that the webhook under **Settings → Webhooks** is still there and pointed at Traceten. If you deleted it, reconnect from the Traceten dashboard.

**Everything shows as unattributed.** Nothing is tying orders back to sessions. Either pass `traceten_visitor_id` through checkout, or call `traceten.identify({ email })` when a visitor signs in.

**Test-mode orders are missing.** Traceten drops test-mode orders in production, because their synthetic amounts would corrupt your revenue totals.

## Next

* [How attribution works](/getting-started/how-it-works)
* [Currencies](/integrations/currencies)
