Skip to content

Lemon Squeezy Integration

Terminal window
pnpm add @smta/billing @lemonsqueezy/lemonsqueezy.js

Instantiate LemonSqueezyProvider with your API key and store ID. The constructor takes two string arguments.

import { LemonSqueezyProvider } from '@smta/billing/lemon-squeezy';
const billing = new LemonSqueezyProvider(
process.env.LEMONSQUEEZY_API_KEY!,
process.env.LEMONSQUEEZY_STORE_ID!
);
VariableDescription
LEMONSQUEEZY_API_KEYYour Lemon Squeezy API key
LEMONSQUEEZY_STORE_IDThe numeric ID of your Lemon Squeezy store
LEMONSQUEEZY_WEBHOOK_SECRETThe signing secret for your webhook endpoint

The createCheckout interface is identical across providers. Lemon Squeezy receives organizationId through the checkout’s custom_data field automatically, so it will be available in every subsequent webhook event.

const { checkoutUrl, sessionId } = await billing.createCheckout({
organizationId: org.id,
planId: 'pro',
priceId: 'variant_1234567890',
billingEmail: user.email,
successUrl: 'https://app.example.com/billing/success',
cancelUrl: 'https://app.example.com/billing/cancel',
});
// Redirect the user to checkoutUrl

Lemon Squeezy signs its webhook payloads with an HMAC signature sent in the x-signature header. Pass the raw body buffer to handleWebhook so the signature can be verified correctly.

import express from 'express';
const app = express();
// Use raw body parser for the webhook route only
app.post(
'/webhooks/lemon-squeezy',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['x-signature'] as string;
let parsed;
try {
parsed = await billing.handleWebhook({
provider: 'lemon_squeezy',
rawBody: req.body, // Buffer — not parsed JSON
signature,
});
} catch (err) {
console.error('Lemon Squeezy webhook error:', err);
return res.status(400).send('Webhook error');
}
await billing.recordSubscriptionUpdate(parsed, db);
res.json({ received: true });
}
);

The provider extracts organizationId from custom_data.organization_id in the webhook payload. This value is injected automatically by createCheckout.

  • platform.billing_customers — org-to-Lemon-Squeezy-customer mapping
  • platform.billing_subscriptions — subscription status per org

Fetch current subscription state:

const subscription = await billing.getSubscription(providerSubscriptionId);
// { plan, status, currentPeriodEnd, cancelAtPeriodEnd, ... }

Cancel a subscription at period end:

await billing.cancelSubscription(providerSubscriptionId);