Skip to main content

LemonSqueezy Integration

Connect your LemonSqueezy account to Windback via the custom webhook endpoint to detect subscription cancellations, failed payments, and successful recoveries.
LemonSqueezy does not have a native Windback integration. This guide uses a lightweight Node.js relay function that receives LemonSqueezy webhooks and forwards them to Windback’s custom webhook endpoint.

Webhook URL

Your Windback custom webhook URL is:
https://api.windbackai.com/api/v1/webhooks/custom/<your_public_key>
Your public key starts with pub_ and is found in Settings > API Keys.

Setup

1

Deploy the Relay Function

Deploy the Node.js relay function below to your server or a serverless platform (Vercel, AWS Lambda, etc.). This function receives LemonSqueezy webhooks, maps the payload, and forwards it to Windback.
2

Add the Webhook in LemonSqueezy

  1. Go to LemonSqueezy Dashboard > Settings > Webhooks
  2. Click Add Webhook (or the + button)
  3. Paste the URL of your deployed relay function
  4. Enter a signing secret and store it securely
  5. Select the events listed below
  6. Click Save
Store the signing secret in an environment variable. You will need it to verify incoming webhook signatures.
3

Select Webhook Events

Enable the following events in LemonSqueezy:
LemonSqueezy EventWindback Event TypeDescription
subscription_cancelledcancellationCustomer canceled their subscription
subscription_payment_failedpayment_failedSubscription payment attempt failed
subscription_payment_successpayment_recoveredPayment succeeded after a previous failure
4

Verify the Connection

  1. Create a test subscription in LemonSqueezy’s test mode
  2. Cancel the subscription to trigger a subscription_cancelled event
  3. Check your Windback dashboard for the new churn event

Data Mapping

LemonSqueezy webhooks use a nested data.attributes format. The relay function maps these fields to Windback’s custom webhook format:
LemonSqueezy FieldWindback FieldNotes
data.attributes.user_emailcustomer_emailSubscriber email address
data.attributes.user_namecustomer_nameSubscriber name
data.attributes.product_nameplan_nameLemonSqueezy product name
data.attributes.variant_nameplan_name (fallback)Used if product name is not set
data.attributes.first_subscription_payment.amountmrrAmount in cents
data.attributes.first_subscription_payment.currencycurrencyISO 4217 code
data.attributes.created_attenure_daysCalculated from subscription start

Relay Function

const express = require("express");
const app = express();
app.use(express.json());

const WINDBACK_URL =
  "https://api.windbackai.com/api/v1/webhooks/custom/pub_your_key";

const EVENT_MAP = {
  subscription_cancelled: "cancellation",
  subscription_payment_failed: "payment_failed",
  subscription_payment_success: "payment_recovered",
};

app.post("/lemonsqueezy-webhook", async (req, res) => {
  const { meta, data } = req.body;
  const eventName = meta?.event_name;

  const windbackEventType = EVENT_MAP[eventName];
  if (!windbackEventType) {
    return res.status(200).json({ status: "skipped" });
  }

  const attrs = data?.attributes || {};

  const tenureDays = attrs.created_at
    ? Math.floor(
        (Date.now() - new Date(attrs.created_at).getTime()) / (1000 * 86400)
      )
    : undefined;

  // LemonSqueezy provides amount in the subscription payment object or
  // as a top-level attribute depending on the event type
  const payment = attrs.first_subscription_payment || {};

  const payload = {
    customer_email: attrs.user_email,
    customer_name: attrs.user_name || undefined,
    event_type: windbackEventType,
    mrr: payment.amount || attrs.subtotal || undefined,
    currency: (payment.currency || attrs.currency || "usd").toLowerCase(),
    provider: "lemonsqueezy",
    plan_name: attrs.product_name || attrs.variant_name || undefined,
    tenure_days: tenureDays,
  };

  try {
    await fetch(WINDBACK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
  } catch (err) {
    console.error("Failed to forward to Windback:", err);
  }

  res.status(200).json({ status: "ok" });
});

app.listen(3000, () =>
  console.log("LemonSqueezy relay listening on port 3000")
);
Replace pub_your_key with your actual Windback public key. Never commit your real key to version control.

Webhook Resilience

Windback’s custom webhook endpoint always returns HTTP 200 regardless of internal processing status. Your relay function should also always return 200 to LemonSqueezy to prevent webhook retries and eventual deactivation.
LemonSqueezy signs webhook payloads using an HMAC signature in the X-Signature header. We strongly recommend verifying this signature in your relay function before forwarding to Windback. See LemonSqueezy’s webhook docs for details.