The missing billing layer for Lemon Squeezy — one function call to handle checkout, webhooks, subscriptions, and licenses.
npm install fresh-squeezyCallbacks are yours to implement — event delivers typed data, you write the business logic:
import { createBilling } from "fresh-squeezy";
const billing = await createBilling({
apiKey: process.env.LS_API_KEY!,
webhookSecret: process.env.LS_WEBHOOK_SECRET!,
callbacks: {
onOrder: async (event, method) => {
// event.userId — your internal user ID (the one you pass at checkout)
// event.email — customer email
// event.orderId — Lemon Squeezy order ID
// event.price — amount in cents
if (method === "purchase") {
// e.g. await prisma.user.update({ where: { id: event.userId }, data: { isPro: true } })
}
if (method === "refund") {
// e.g. await prisma.user.update({ where: { id: event.userId }, data: { isPro: false } })
}
},
onSubscription: async (event, method) => {
// event.userId — your internal user ID
// event.subscriptionId — Lemon Squeezy subscription ID
// event.status — active | cancelled | expired | paused
if (method === "created") {
/* activate subscription in your DB */
}
if (method === "cancelled") {
/* mark subscription cancelled */
}
if (method === "payment_success") {
/* extend access, clear dunning flags */
}
if (method === "payment_failed") {
/* notify customer, start dunning */
}
},
onLicenseKey: async (event, method) => {
// event.userId — your internal user ID
// event.key — the license key string to store and share with the user
// event.status — active | inactive | expired
if (method === "created") {
/* store event.key in your DB */
}
},
},
});
// Checkout URL
const url = await billing.createCheckout({
variantId: billing.plans[0].variantId,
email: "user@example.com",
userId: "user_abc",
});
// Webhook endpoint (Express)
app.post(
"/webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
if (
!billing.verifyWebhook(
req.body.toString(),
req.headers["x-signature"] as string,
)
)
return res.sendStatus(401);
await billing.handleWebhook(JSON.parse(req.body.toString()));
res.json({ ok: true });
},
);The fastest way to get started — auto-discovers your stores and products, generates ready-to-run config files, and optionally runs API lifecycle validation tests:
npx fresh-squeezy-billing wizardGenerates .billing/billing-config.ts and .billing/example.ts. Previous answers are cached to .billing/wizard-cache.json for fast re-runs.
Powered by grimoire-wizard
The wizard experience is built on top of grimoire-wizard — a config-driven CLI wizard framework for Node.js. Define interactive terminal wizards in YAML with back-navigation, conditional branching, validation, and structured output. If you need a wizard UI for your own CLI tool, check it out.
interface BillingConfig {
apiKey: string;
webhookSecret?: string;
storeId?: string;
cachePath?: string; // default: ".billing/cache.json"
cacheTtlMs?: number; // default: 3_600_000 (1 hour)
checkoutExpiresInMs?: number;
logger?: { filePath: string };
callbacks: BillingCallbacks;
dedup?: DedupConfig;
}4 callbacks, all using the same (event, method) pattern:
interface BillingCallbacks {
// required — handles purchase and refund
onOrder: (event: OrderEvent, method: "purchase" | "refund") => Promise<void>;
// optional — all subscription lifecycle + payment events
onSubscription?: (
event: SubscriptionEvent,
method: SubscriptionMethod,
) => Promise<void>;
// optional — license key created or updated
onLicenseKey?: (
event: LicenseKeyEvent,
method: "created" | "updated",
) => Promise<void>;
// optional — catch-all for every raw webhook event
onWebhook?: (eventType: string, event: WebhookEvent) => Promise<void>;
}
type SubscriptionMethod =
| "created"
| "updated"
| "cancelled"
| "expired" // lifecycle
| "paused"
| "resumed" // pausing
| "payment_success"
| "payment_recovered"
| "payment_failed"; // paymentsbilling.stores; // StoreInfo[]
billing.plans; // Plan[]
billing.createCheckout(params); // → checkout URL
billing.verifyWebhook(rawBody, signature); // → boolean
billing.handleWebhook(payload); // dispatches to callbacks
billing.refreshPlans(); // refresh cached plans
billing.getCustomerPortal(customerId); // → portal URL
billing.getExpressRouter(options); // Express Router
billing.healthCheck(); // → HealthCheckResultbilling.getStore(storeId); // → store details
billing.listStores(); // → all stores
billing.getAuthenticatedUser(); // → API key ownerbilling.getSubscription(id)
billing.listSubscriptions(filter?)
billing.pauseSubscription(id, reason?)
billing.resumeSubscription(id)
billing.cancelSubscription(id, immediately?)
billing.changeSubscriptionVariant(id, variantId)
billing.resumeCancelledSubscription(id)
// Invoices
billing.getSubscriptionInvoice(invoiceId)
billing.listSubscriptionInvoices(filter?)
billing.generateSubscriptionInvoice(invoiceId)
billing.issueSubscriptionInvoiceRefund(invoiceId, amount)
// Metered billing
billing.getSubscriptionItem(itemId)
billing.listSubscriptionItems(subscriptionId?)
billing.getSubscriptionItemCurrentUsage(itemId)
billing.createUsageRecord(subscriptionItemId, quantity, action?)
billing.listUsageRecords(subscriptionItemId?)billing.getOrder(orderId)
billing.listOrders(filter?) // filter: { storeId?, userEmail? }
billing.generateOrderInvoice(orderId)
billing.issueOrderRefund(orderId, amount)
billing.getOrderItem(orderItemId)
billing.listOrderItems(filter?)billing.getCustomer(customerId);
billing.getCustomerByEmail(email);
billing.createCustomer(storeId, name, email);
billing.updateCustomer(customerId, params); // { name?, email? }
billing.archiveCustomer(customerId);
billing.getSubscriptionsForUser(userId);billing.validateLicense(key) // → { valid, details? }
billing.getLicenseDetails(key) // → LicenseKeyEvent | null
billing.activateLicense(key, instanceName?) // → { activated, instanceId? }
billing.deactivateLicense(key, instanceId) // → boolean
billing.listLicenseKeys(filter?)
billing.getLicenseKeyInstance(instanceId)
billing.listLicenseKeyInstances(licenseKeyId?)
billing.updateLicenseKey(licenseKeyId, params) // { disabled?, activationLimit? }billing.listWebhooks() // → [{ id, url, events, createdAt }]
billing.getWebhook(webhookId) // → { id, url, events, createdAt } | null
billing.createWebhook(url, events, secret?) // → webhook ID
billing.updateWebhook(webhookId, url?, events?) // → void
billing.deleteWebhook(webhookId) // → voidbilling.getProduct(productId)
billing.listProducts(storeId?)
billing.getVariant(variantId)
billing.listVariants(productId?)
billing.getPrice(priceId)
billing.listPrices(variantId?)
billing.getFile(fileId)
billing.listFiles(variantId?)billing.createDiscount(storeId, params) // params: { name, code, amount, amountType?, expiresAt? }
billing.deleteDiscount(discountId)
billing.getDiscount(discountId)
billing.listDiscounts(storeId?)
billing.getDiscountRedemption(redemptionId)
billing.listDiscountRedemptions(discountId?)billing.createCheckout(params) // → URL string
billing.getCheckout(checkoutId)
billing.listCheckouts(storeId?, variantId?)Drop-in router for /plans and /checkout endpoints:
app.use(
"/billing",
billing.getExpressRouter({
getUserId: (req) => req.user.id,
getUserEmail: (req) => req.user.email,
}),
);
// GET /billing/plans → returns billing.plans
// POST /billing/checkout → returns { url }
// POST /billing/webhook → handles + verifies webhookWebhook deduplication is in-memory by default. Swap to Redis or DB for multi-instance:
import { RedisDedupBackend } from "fresh-squeezy";
const billing = await createBilling({
...config,
dedup: { backend: new RedisDedupBackend(redisClient), ttlMs: 86_400_000 },
});npx fresh-squeezy-billing wizard # interactive setup wizard
npx fresh-squeezy-billing validate # smoke-test your config
npx fresh-squeezy-billing helpCopy the block below and paste it into Claude, ChatGPT, or any LLM to get instant help integrating fresh-squeezy into your project:
I'm integrating the npm package "fresh-squeezy" (Lemon Squeezy billing layer) into my project.
Package: fresh-squeezy
Install: npm install fresh-squeezy
The main API:
import { createBilling } from "fresh-squeezy";
const billing = await createBilling({
apiKey: process.env.LS_API_KEY,
webhookSecret: process.env.LS_WEBHOOK_SECRET,
callbacks: {
onOrder: async (event, method) => {
// method: 'purchase' | 'refund'
// event: { userId, email, orderId, customerId?, variantId?, productName?, price? }
},
onSubscription: async (event, method) => {
// method: 'created'|'updated'|'cancelled'|'expired'|'paused'|'resumed'
// | 'payment_success'|'payment_recovered'|'payment_failed'
// event: { userId, email, subscriptionId, customerId, variantId, status, ... }
},
onLicenseKey: async (event, method) => {
// method: 'created' | 'updated'
// event: { userId, email, key, licenseKeyId, productId, variantId, status }
},
},
});
billing.plans // available plans/variants
billing.stores // store info
billing.createCheckout({ variantId, email, userId }) // → checkout URL
billing.verifyWebhook(rawBody, signature) // → boolean
billing.handleWebhook(payload) // dispatches to callbacks
billing.getCustomerPortal(customerId) // → portal URL
// Management (all return typed data)
billing.listWebhooks()
billing.getSubscription(id) / listSubscriptions(filter?)
billing.listOrders(filter?) / getOrder(id) / issueOrderRefund(id, amount)
billing.getCustomer(id) / createCustomer(storeId, name, email)
billing.validateLicense(key) / activateLicense(key, instanceName)
billing.listProducts(storeId?) / listVariants(productId?)
billing.createDiscount(storeId, { name, code, amount })
Webhook endpoint pattern (Express):
app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
if (!billing.verifyWebhook(req.body.toString(), req.headers["x-signature"])) return res.sendStatus(401);
await billing.handleWebhook(JSON.parse(req.body.toString()));
res.json({ ok: true });
});
Help me [describe what you need help with].
Thanks to all the amazing contributors who have helped make this project better! 🎉
- Node.js >= 20
@lemonsqueezy/lemonsqueezy.js>= 3.2.0 (peer dep)- Express >= 4.18 (optional — only for
getExpressRouter)
MIT