Tutorial

Automating License Provisioning with LemonSqueezy Webhooks

TOT
Traffic Orchestrator Team
Engineering
March 25, 2026 11 min read 1,022 words
Share

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.

Merchant of Record Advantage: Unlike Stripe where you're the merchant, LemonSqueezy is the seller. This means webhook events come from LemonSqueezy's systems, not directly from payment processors — simplifying your integration to a single source of truth.

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...
}
Never skip signature verification. Without it, anyone can send fake webhook payloads to your endpoint and grant themselves licenses. Always verify before processing.

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 license
  • subscription_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.id combinations 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:

  1. Enable Test Mode in your LemonSqueezy dashboard
  2. Create a webhook pointing to your staging endpoint
  3. Use the test card number 4242 4242 4242 4242 to complete a purchase
  4. 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.

TOT
Traffic Orchestrator Team
Engineering

The engineering team behind Traffic Orchestrator, building enterprise-grade software licensing infrastructure used by developers worldwide.

Was this article helpful?
Get licensing insights delivered

Engineering deep-dives, security advisories, and product updates. Unsubscribe anytime.

Share this article
Free Plan Available

Ship licensing in your next release

5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.

2-minute setup No credit card Cancel anytime