# Revenue from Lemon Squeezy

> Connect one Lemon Squeezy store and read MRR, payments, cancellations and which source each paying customer came from. Launch only ever reads.

**Note:**
Every plan can connect Lemon Squeezy, with the same history as Stripe: **Free** reads the last 30 days and refreshes every six hours; **Indie** reads 90 days and **Builder** 24 months, refreshed hourly. Free and Indie include one Lemon Squeezy store per account, Builder one per project. The limit is per provider, so a project can have Stripe and Lemon Squeezy both.

If you sell through Lemon Squeezy, Nordva Launch can read one of your stores and report the same numbers it reports for [Stripe](/guides/revenue-stripe): MRR, who started paying, who cancelled, what was collected, and in [Contacts](/guides/contacts) and [Attribution](/guides/attribution), who each paying customer is and which source they came from.

## What Launch reads, and what it never does

Launch reads, for the one store you pick:

| Lemon Squeezy object | What Launch keeps |
|---|---|
| Customers | Email, name, country, when they became a customer |
| Subscriptions | Status, product and variant name, price, renewal interval, when it started, trial end, when it ends |
| Subscription invoices | Every subscription charge, the first one included: amount, currency, date, refunds |
| Orders | One-time purchases: amount, currency, date, refunds |
| Prices, variants, the store | Looked up by id to work out MRR and to tell one-time orders from subscription orders |

Launch **never** writes to Lemon Squeezy: no refunds, no cancellations, no changes to customers, subscriptions, products or webhooks. It never sees card data.

A Lemon Squeezy API key cannot be limited to reading, the way a Stripe restricted key can. Whoever holds the key can do what its owner can. So the promise is about Launch, not the key: every request Launch makes to Lemon Squeezy goes through one client that can only send `GET`, and our tests check every request the sync and the connect screen make. The key is encrypted at rest, is never shown again, never appears in a response, a log or an AI conversation, and is deleted when you disconnect.

## 1. Make an API key in Lemon Squeezy

1. In Lemon Squeezy, open **Settings → API** ([app.lemonsqueezy.com/settings/api](https://app.lemonsqueezy.com/settings/api)).
2. To try this with test data first, switch **Test mode** on in the Lemon Squeezy dashboard before making the key. A test-mode key only sees test-mode data; Launch shows the connection as test mode.
3. Click **+** to add a key, name it `Nordva Launch`, and create it.
4. Copy the key. Lemon Squeezy shows it once. It is a long string starting `eyJ`.

Lemon Squeezy API keys are valid for a year. When yours expires, Launch marks the connection as stopped and emails you; connect a new key and the data catches up.

## 2. Connect it in the dashboard

Open [**Settings → Revenue sources**](https://app.nordva.dev/revenue), choose **Lemon Squeezy**, paste the key and click **Find stores**. Launch checks the key, lists the stores it can see and whether it is a test key, and stores nothing yet. Pick the store and click **Connect store**. Launch then reads one of each list it needs from that store; if Lemon Squeezy refuses one, you see which, and nothing is saved.

Connecting is only possible in the dashboard, behind your login. There is no API-key endpoint and no MCP tool for it.

The first import starts immediately and runs in the background.

## 3. Optional: link purchases to visits

Launch matches a paying customer to the visitor they came from by email: an email sent with [identify](/guides/web-analytics), a form with `data-nordva-identify`, or the same email on your waitlist. That covers people who signed up or logged in first.

To also cover people who went straight from your site to checkout, the analytics script adds the visitor id to your Lemon Squeezy checkout links, and Lemon Squeezy sends it back in a webhook:

1. Under **Link purchases to visits** on the Lemon Squeezy card, click **Set up webhook**. Launch shows a URL and a signing secret, once.
2. In Lemon Squeezy, open **Settings → Webhooks** and click **+**.
3. Paste the URL as the callback URL and the secret as the signing secret, tick `order_created` and `subscription_created`, and save.

Once a signed delivery arrives, the card shows when. Launch reads only four things from it: the store, the customer id, the customer's email and name, and the visitor id. It verifies the `X-Signature` (HMAC-SHA256 of the body with your secret) before reading anything, and does not keep the rest of the payload.

How the visitor id gets to Lemon Squeezy: when a visitor who may carry an analytics id (see [tracking modes](/guides/web-analytics)) clicks a link to `https://<your-store>.lemonsqueezy.com/checkout/...` or `/buy/...`, the script adds `checkout[custom][nv_vid]=<visitor id>` to the link. Nothing is added for a visitor without an id. If you create checkouts on your server with Lemon Squeezy's API instead, put the id there yourself:

```ts
// In the browser, before you ask your server for a checkout:
const visitor = window.nordva?.visitor?.();
// On your server, in the checkout you create:
checkout_data: { custom: { nv_vid: visitorIdFromTheBrowser } }
```

A checkout on a custom domain of your own is not changed by the script; use the server way above.

## How the numbers are counted

- **Money is before tax, after discounts.** For each order and subscription invoice Launch stores `total - tax`. A partial refund scales it down by `refunded_amount / total`. Fully refunded, pending, failed, void and fraudulent charges are not payments; a charge refunded after it was imported is removed at the next sync.
- **No double counting.** A subscription's first payment appears in Lemon Squeezy both as an order and as the subscription's first invoice. Launch counts subscription money from subscription invoices only, and orders only for one-time products.
- **MRR** comes from each subscription's price: unit price × quantity over the renewal interval (yearly ÷ 12, weekly × 52 ÷ 12), through the same maths as Stripe. Usage-based, graduated and volume prices count as zero, and so does a paused subscription. Lemon Squeezy does not expose a subscription's discount through its API, so MRR is at the list price.
- **Currencies.** Payments stay in the currency the buyer paid in; MRR is in the store's currency. Nothing is converted.
- **Statuses** use the same words as Stripe everywhere in Launch:

| Lemon Squeezy | In Launch |
|---|---|
| `on_trial` | `trialing` (not in MRR) |
| `active` | `active` |
| `cancelled` | `active`, cancelling at the end of the period (it is still paid up until `ends_at`) |
| `expired` | `canceled`, ended at `ends_at` |
| `past_due`, `unpaid`, `paused` | the same |

With Stripe and Lemon Squeezy both connected, every figure adds both per currency, and `sync.providers` in each response lists the two connections. A person paying through both counts as two customers unless the same email links them in Contacts.

## Keeping up to date

Lemon Squeezy has no change feed, so each hourly sync reads every subscription (their status is what changes), customers new since the last full import, and orders and invoices from 45 days before it (refunds land on older charges). The full import runs again every week, which picks up anything else, such as a changed email. A store too large to refresh within one sync's request budget (about 2,500 subscriptions) falls back to a full import every run: the numbers stay right, but the status shows importing more often. The webhook above also makes the next sync start within a minute of a purchase.

## Reading it

The endpoints are the same as for Stripe. `GET /v1/integrations/lemonsqueezy` returns the connection's status; the revenue endpoints include Lemon Squeezy rows, and `GET /v1/revenue/events` says which provider each row came from.

```bash
curl "https://api.nordva.dev/v1/integrations/lemonsqueezy" \
  -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"
```

```json
{
  "data": {
    "provider": "lemonsqueezy",
    "connected": true,
    "status": "active",
    "livemode": false,
    "store_name": "Lemonade Stand",
    "last_synced_at": "2026-09-27T14:02:11.000Z",
    "synced": { "customers": 214, "subscriptions": 61, "payments": 488 },
    "webhook": { "configured": true, "last_received_at": "2026-09-27T13:58:40.000Z" }
  }
}
```

From an MCP client, `get_lemonsqueezy_connection_status` reports the same, and the revenue tools read both providers. The assistant cannot connect Lemon Squeezy and will never ask you for a key.

## Disconnecting

**Disconnect** deletes the stored key, the webhook secret and every synced Lemon Squeezy customer, subscription and payment, and removes contacts who were only known from them. Stripe data, if connected, stays. Nothing changes in Lemon Squeezy: delete the API key and the webhook there as well. Deleting the project does the same.

## Errors

| Code | Meaning |
|---|---|
| `LEMONSQUEEZY_KEY_INVALID` | Lemon Squeezy did not accept the key: expired, deleted or copied incompletely. |
| `LEMONSQUEEZY_KEY_MISSING_ACCESS` | Lemon Squeezy refused one of the reads. The message says which. Make the key as the store's owner or an admin. |
| `LEMONSQUEEZY_STORE_NOT_FOUND` | The store is not one this key can see. A test key only sees test data. |
| `LEMONSQUEEZY_ALREADY_CONNECTED` | This project already has a store connected. Disconnect it first. |
| `LEMONSQUEEZY_CONNECTION_LIMIT_REACHED` | Free and Indie include one Lemon Squeezy store per account. Builder includes one per project. |
| `STRIPE_NOT_CONNECTED` | A revenue endpoint was called with neither Stripe nor Lemon Squeezy connected. The code name is older than Lemon Squeezy support. |

Source: https://docs.nordva.dev/guides/revenue-lemonsqueezy/
