Why LemonSqueezy for Software Licensing
LemonSqueezy has quickly become a favorite among indie developers and small SaaS companies. As a merchant of record, it handles sales tax, VAT, and compliance for you — which means you can focus on building your product instead of navigating international tax law.
But while LemonSqueezy handles payments beautifully, it doesn't manage license keys for domain-bound software, activation limits, or feature-gated tiers. That's where connecting it to a dedicated license management system pays off: customers purchase through LemonSqueezy, and their license is provisioned instantly via webhooks.
How LemonSqueezy Webhooks Work
LemonSqueezy webhooks follow a familiar pattern: when an event occurs (purchase, subscription update, refund), LemonSqueezy sends a signed HTTP POST to your endpoint with the event data as JSON.
Key LemonSqueezy Events for License Management
LemonSqueezy uses a clean event naming scheme. Here are the events that matter for licensing:
| LemonSqueezy Event | License Action | Priority |
|---|---|---|
order_created |
Create new license key | Critical |
subscription_updated |
Upgrade/downgrade license tier | High |
subscription_cancelled |
Schedule license revocation | Critical |
subscription_payment_success |
Extend license expiry | High |
subscription_payment_failed |
Flag license for grace period | Medium |
order_refunded |
Revoke license immediately | Critical |
Verifying Webhook Signatures
LemonSqueezy signs webhooks using HMAC-SHA256. You define the signing secret when creating the webhook
in your LemonSqueezy dashboard, and every incoming payload includes an X-Signature header.
import { createHmac } from 'crypto'
const verifyLemonSqueezySignature = (
payload: string,
signature: string,
secret: string
): boolean => {
const hmac = createHmac('sha256', secret)
const digest = hmac.update(payload).digest('hex')
return digest === signature
}
// In your webhook handler
const handleWebhook = async (c) => {
const signature = c.req.header('x-signature')
const body = await c.req.text()
if (!verifyLemonSqueezySignature(body, signature, WEBHOOK_SECRET)) {
return c.json({ error: 'Invalid signature' }, 401)
}
const event = JSON.parse(body)
// Process the event...
}
Creating Licenses on Purchase
The order_created event fires when a customer completes checkout. The payload includes
the customer's email, the product/variant they purchased, and the order details:
const handleOrderCreated = async (event) => {
const { data } = event
const customerEmail = data.attributes.user_email
const customerName = data.attributes.user_name
const variantId = data.attributes.first_order_item
?.variant_id
// Map LemonSqueezy variant to license tier
const tier = mapVariantToTier(variantId)
// Create the license via Traffic Orchestrator API
const license = await fetch(
'https://api.trafficorchestrator.com/api/v1/licenses',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
product_id: 'your-product-id',
customer_email: customerEmail,
customer_name: customerName,
tier: tier.name,
max_domains: tier.domains,
metadata: {
lemonsqueezy_order_id: data.id,
lemonsqueezy_customer_id:
data.attributes.customer_id
}
})
}
)
console.log('License provisioned for', customerEmail)
}
Handling Subscription Changes
LemonSqueezy handles plan changes through the subscription_updated event. The payload
includes the new variant ID, which you map to your internal tier system:
const handleSubscriptionUpdate = async (event) => {
const { data } = event
const newVariantId = data.attributes.variant_id
const customerId = data.attributes.customer_id
const status = data.attributes.status
// Only process active subscriptions
if (status !== 'active') return
const newTier = mapVariantToTier(newVariantId)
const license = await findLicenseByLemonSqueezyId(
customerId
)
if (!license) return
await fetch(
`https://api.trafficorchestrator.com/api/v1/licenses/${license.id}`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tier: newTier.name,
max_domains: newTier.domains,
features: newTier.features
})
}
)
}
Handling Refunds and Cancellations
LemonSqueezy being a merchant of record means refunds are handled differently than with Stripe. When LemonSqueezy processes a refund, your software should respond accordingly:
order_refunded: Immediately revoke or downgrade the licensesubscription_cancelled: Keep the license active until the billing period ends, then revoke
const handleRefund = async (event) => {
const orderId = event.data.id
const license = await findLicenseByOrderId(orderId)
if (!license) return
// Revoke immediately for refunds
await fetch(
`https://api.trafficorchestrator.com/api/v1/licenses/${license.id}`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'revoked',
revocation_reason: 'refund'
})
}
)
}
Variant-to-Tier Mapping
LemonSqueezy uses variants instead of Stripe's price IDs. Each product can have multiple variants representing different tiers:
const VARIANT_TIER_MAP = {
12345: { name: 'starter', domains: 3, features: ['basic'] },
12346: { name: 'pro', domains: 10, features: ['basic', 'api'] },
12347: { name: 'business', domains: 25, features: ['basic', 'api', 'whitelabel'] },
}
const mapVariantToTier = (variantId) => {
return VARIANT_TIER_MAP[variantId] ?? {
name: 'starter', domains: 3, features: ['basic']
}
}
Idempotency Best Practices
Like all webhook systems, LemonSqueezy may deliver events more than once. Your handler must handle duplicates gracefully:
- Track processed event IDs — Store
meta.event_name+data.idcombinations in your database or KV store - Use UPSERT operations — Update existing licenses rather than creating duplicates
- Return 200 fast — Acknowledge webhooks within 5 seconds to prevent retries; queue heavy processing for background execution
Testing Your Integration
LemonSqueezy provides a test mode with its own API keys and dashboard. Use it to simulate the full purchase flow without real charges:
- Enable Test Mode in your LemonSqueezy dashboard
- Create a webhook pointing to your staging endpoint
- Use the test card number
4242 4242 4242 4242to complete a purchase - Verify the webhook fires and your license provisioning works end-to-end
Production Checklist
Before going live:
- ☑ Webhook signature verification is enabled and tested
- ☑ All critical events are handled (order_created, subscription_updated, subscription_cancelled, order_refunded)
- ☑ Idempotency prevents duplicate license creation
- ☑ Grace period logic is implemented for failed payments
- ☑ Webhook endpoint responds within 5 seconds
- ☑ Test mode verified end-to-end
- ☑ Monitoring and alerting configured
Traffic Orchestrator + LemonSqueezy
Traffic Orchestrator's webhook endpoint accepts events from any payment provider — including LemonSqueezy. Configure your LemonSqueezy webhook URL, map your variants to license tiers in the dashboard, and every purchase automatically provisions a domain-bound license key.
- Zero-code setup — Configure everything in the dashboard, no webhook handler code needed
- Instant delivery — License keys are emailed to customers within seconds of purchase
- Smart upgrades — Plan changes automatically adjust domain limits and feature access
- Refund protection — Automatic license revocation on refund, grace periods on payment failures
Get started free → and connect LemonSqueezy in under 5 minutes.
Ship licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.