CHURN METRICSLEMON SQUEEZY

Tracking churn with Lemon Squeezy webhooks

Which Lemon Squeezy webhook events mark a cancellation, an expiry, a failed payment or a recovery, and how to turn them into a reliable churn log.

Published 5 min read

If you sell subscriptions through Lemon Squeezy, every churn signal you need passes through its webhooks: cancellations, expiries, failed renewals, recoveries, pauses. The hard part is not receiving them. It is deciding which event means what, and recording them so that your churn numbers still add up six months later.

This guide maps Lemon Squeezy’s subscription events to churn concepts, shows how to store them, and lists the delivery details that break naive handlers. It sticks to Lemon Squeezy’s own documentation: the webhook event types, the webhook requests page, signing requests, the sync with webhooks guide and the Subscription object reference (all accessed November 2026).

The subscription events

Lemon Squeezy’s event types page lists these subscription events:

Event What Lemon Squeezy says triggers it Churn meaning
subscription_created A new subscription New (or returning) customer
subscription_updated Any change after the initial payment Use to sync status
subscription_cancelled Cancelled by the customer or store owner; enters a grace period until the next billing date Churn scheduled, not yet happened
subscription_resumed Resumed after being cancelled Scheduled churn reversed
subscription_expired Ended after being cancelled, or once dunning completed for past_due subscriptions Churn happened
subscription_paused Payment collection paused Not churn; track separately
subscription_unpaused Payment collection resumed End of pause
subscription_payment_success A successful payment Revenue
subscription_payment_failed A renewal payment failed Involuntary churn risk
subscription_payment_recovered A successful payment after a failed one Risk resolved
subscription_payment_refunded A payment refunded Revenue adjustment

Two of these deserve a closer look.

Cancelled is not churned

subscription_cancelled fires when the customer clicks cancel, but the subscription keeps running until ends_at. During that grace period it can be resumed, which sends subscription_resumed. If you count churn on subscription_cancelled, every resumed subscription becomes a false churn, and your churn date is off by up to a full billing period.

Count churn on subscription_expired. Keep subscription_cancelled as a separate, leading metric: “cancellations scheduled this month”.

One event, two kinds of churn

subscription_expired fires for two very different reasons: a customer who cancelled and reached ends_at (voluntary churn), and a subscription that went through dunning after failed payments without recovering (involuntary churn). The event name does not tell you which.

To separate them, look at what came before. If your log has a subscription_cancelled for that subscription and no subscription_resumed after it, it is voluntary. If it has subscription_payment_failed events and no recovery, it is involuntary. You can also read the subscription’s state when you receive the event: the Subscription object reference shows a cancelled boolean alongside status.

Voluntary and involuntary churn need different fixes, so this split is the most useful thing your log can give you.

What the requests look like

Per the webhook requests page:

  • Each request is a POST with a JSON:API body: the resource related to the event (a subscription for most subscription events, a subscription invoice for the payment events).
  • The X-Event-Name header carries the event name.
  • The X-Signature header carries the signature.
  • Your endpoint must return 200. Any other status is retried up to three more times with exponential backoff (the docs give 5, 25 and 125 seconds as an example), after which the request is considered failed.

The webhooks settings page in your dashboard logs recent deliveries with their payloads and lets you resend them. That is your safety net if your endpoint was down.

Verify the signature

Signing requests explains the scheme: you set a signing secret when you create the webhook (6 to 40 characters), and Lemon Squeezy computes an HMAC-SHA256 hex digest of the request body with it. Recompute the digest on the raw body, not on re-serialized JSON, and compare with a timing-safe function. Reject anything that does not match.

Payment events carry invoices

The subscription_payment_* events carry a subscription invoice object, not the subscription. It references the subscription, so store that ID and, if you need the status, fetch the subscription.

Building a churn log step by step

The goal is an append-only table of what happened to each subscription, from which every churn metric can be recomputed.

  1. Create one endpoint for all subscription events. Subscribe it to the events in the table above. Lemon Squeezy’s sync guide notes that subscription_updated covers every change, so you can subscribe to it alone if you only want state. For churn tracking, the specific events are easier to reason about, so take both.
  2. Verify, then store the raw payload. Write the event name, the subscription ID, the customer ID, the timestamp and the raw JSON before doing anything else.
  3. Deduplicate. Retries and manual resends mean the same event can arrive twice. The docs we used do not describe a unique event ID in the request, so build your own key from the event name, the resource ID and the resource’s updated_at, and ignore repeats.
  4. Respond fast. Return 200 as soon as the event is stored, and process it in a background job. A slow handler that times out triggers retries.
  5. Derive, do not overwrite. Keep the log immutable. Compute “current status” and churn counts from it, so a bug in your logic can be fixed by replaying the log.
  6. Reconcile occasionally. Once a week, list subscriptions through the API and compare their statuses to what your log says. Any mismatch means a missed event.

The metrics you can compute

With that log, these are straightforward:

  • Customer churn rate for a month: subscriptions that expired during the month, divided by subscriptions active at the start of the month.
  • Voluntary vs involuntary share: expiries split by the rule above.
  • Cancellations scheduled: subscription_cancelled events in the month, a leading indicator for next month’s churn.
  • Grace-period saves: cancelled subscriptions followed by subscription_resumed.
  • Payment recovery rate: subscriptions with a subscription_payment_recovered divided by subscriptions with a subscription_payment_failed in the same period.
  • Paused share: subscriptions currently paused. Report them separately; they are neither active payers nor churned.

Define each metric once, in writing, and keep the definition stable. A churn rate is only useful compared to itself over time.

Testing before you trust it

Lemon Squeezy’s test mode can simulate webhook events for subscriptions. Run each lifecycle through it: create, cancel, resume, cancel again and let it expire; a failed payment that recovers; one that does not. Check that your log and your metrics match what you expect at every step.

Doing this with NoChurn

Lemon Squeezy stays the source of truth for subscriptions and payments; NoChurn does not change them. NoChurn subscribes to your Lemon Squeezy webhooks, stores and deduplicates the events, and separates voluntary from involuntary churn for you. On top of that it adds a churn score from 0 to 100 per customer, custom data you send by API, and the cancel flow answers that explain why people leave. Disputes, which Lemon Squeezy handles as the merchant of record, are shown read only. See NoChurn + Lemon Squeezy.

PROMPT

Read https://www.nochurn.sh/llms.txt,
then follow https://www.nochurn.sh/docs/quickstart/
to add NoChurn to this app. Use a test key
(nc_test_…) until I say we go live.

Stop leaking revenue.

  • Free until $300 MRR
  • No card needed
  • Live in 10 minutes
  • Never a cut of what we recover