Tutorial

Automating License Provisioning with Stripe Webhooks

TOT
Traffic Orchestrator Team
Engineering
March 25, 2026 12 min read 1,277 words
Share

The Manual Licensing Bottleneck

Every software business that sells licenses eventually hits the same wall: a customer buys your product at 2 AM, and their license key arrives... when you wake up and check your email. Or worse, it never arrives because the order notification got buried.

Manual license provisioning doesn't scale. It creates support tickets, delays onboarding, and costs you customers who expected instant access. The solution is webhook-driven automation — connecting your payment processor directly to your license management system so that every purchase, upgrade, downgrade, and cancellation triggers the right license action automatically.

Architecture Overview

The integration flow is straightforward:

  1. Customer completes checkout via Stripe
  2. Stripe fires a webhook event to your server
  3. Your webhook handler validates the event signature
  4. Based on the event type, your handler creates, modifies, or revokes the license
  5. The customer receives their license key via email — instantly
Why Stripe Webhooks? Unlike polling or manual checks, webhooks are event-driven. You're notified in real-time when something happens — no delays, no missed events, no wasted API calls.

Key Stripe Events for License Management

Not every Stripe event matters for licensing. Here are the ones you need to handle:

Stripe Event License Action Priority
checkout.session.completed Create new license key Critical
customer.subscription.updated Upgrade/downgrade license tier High
customer.subscription.deleted Revoke or downgrade license Critical
invoice.payment_succeeded Extend license expiry High
invoice.payment_failed Flag license for grace period Medium
customer.subscription.paused Suspend license (keep data) Medium

Setting Up Your Webhook Endpoint

Your webhook endpoint needs to do three things: verify the signature, parse the event, and dispatch it to the correct handler. Here's the foundation:

// Webhook handler — works with any TypeScript framework
const handleStripeWebhook = async (c) => {
  const sig = c.req.header('stripe-signature')
  const body = await c.req.text()

  // 1. Verify webhook signature
  const event = await stripe.webhooks.constructEventAsync(
    body,
    sig,
    WEBHOOK_SECRET
  )

  // 2. Dispatch to handler based on event type
  switch (event.type) {
    case 'checkout.session.completed':
      await handleNewPurchase(event.data.object)
      break
    case 'customer.subscription.updated':
      await handleSubscriptionChange(event.data.object)
      break
    case 'customer.subscription.deleted':
      await handleCancellation(event.data.object)
      break
    case 'invoice.payment_failed':
      await handlePaymentFailure(event.data.object)
      break
  }

  return c.json({ received: true })
}
Always verify signatures. Never trust raw webhook payloads. Stripe signs every event with your endpoint secret — verify it before processing. Skipping this step opens you to spoofed events that could grant unauthorized licenses.

Creating Licenses on Purchase

The checkout.session.completed event is your primary trigger. When it fires, extract the customer info and product details, then create the license:

const handleNewPurchase = async (session) => {
  const customer = await stripe.customers.retrieve(
    session.customer
  )

  // Map Stripe price ID to your license tier
  const tier = mapPriceToTier(session.line_items)

  // Create the license via 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: customer.email,
        customer_name: customer.name,
        tier: tier,
        max_domains: tier === 'business' ? 25 : 5,
        metadata: {
          stripe_customer_id: session.customer,
          stripe_subscription_id: session.subscription
        }
      })
    }
  )

  // License key is auto-emailed to the customer
  console.log('License created:', license.key)
}

Handling Upgrades and Downgrades

When a customer changes their plan, Stripe fires customer.subscription.updated. Your handler should adjust the license tier accordingly:

const handleSubscriptionChange = async (subscription) => {
  const newTier = mapPriceToTier(subscription.items.data)
  const stripeCustomerId = subscription.customer

  // Find the license by Stripe customer ID
  const license = await findLicenseByStripeId(stripeCustomerId)

  if (!license) return

  // Update the license tier and limits
  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,
        max_domains: getTierDomainLimit(newTier),
        features: getTierFeatures(newTier)
      })
    }
  )
}

Grace Periods for Failed Payments

Don't immediately revoke licenses when a payment fails. Customers often have temporary card issues. Instead, implement a grace period:

  1. First failure: Send a warning email, license remains active
  2. After 3 days: Send a reminder with a payment update link
  3. After 7 days: Downgrade to free tier (preserve data)
  4. After 14 days: Suspend the license entirely
const handlePaymentFailure = async (invoice) => {
  const subscription = await stripe.subscriptions.retrieve(
    invoice.subscription
  )
  const attemptCount = invoice.attempt_count

  if (attemptCount === 1) {
    // First failure — just notify
    await sendPaymentWarningEmail(invoice.customer_email)
  } else if (attemptCount >= 3) {
    // Multiple failures — downgrade to free tier
    const license = await findLicenseByStripeId(
      invoice.customer
    )
    await downgradeLicense(license.id, 'free')
    await sendDowngradeNotification(invoice.customer_email)
  }
}

Idempotency and Reliability

Webhooks can be delivered more than once. Stripe retries failed deliveries up to 3 days. Your handler must be idempotent — processing the same event twice should produce the same result.

Strategies for Idempotency

  • Store processed event IDs — Before processing, check if event.id has already been handled. If so, return 200 immediately.
  • Use database transactions — Wrap license creation in a transaction so partial failures don't create orphaned records.
  • Design for re-entrant operations — Use UPSERT instead of INSERT so duplicate events just update the existing record.
// Idempotency check
const processedEvents = new Set() // Use KV/DB in production

const handleWebhook = async (event) => {
  if (processedEvents.has(event.id)) {
    return { already_processed: true }
  }

  // Process the event...
  await dispatchEvent(event)

  // Mark as processed
  processedEvents.add(event.id)
  // In production: await kv.put(
  //   `webhook:${event.id}`, '1', { expirationTtl: 86400 * 7 }
  // )
}

Price-to-Tier Mapping

A clean mapping between Stripe price IDs and your internal license tiers keeps the webhook handler simple:

const PRICE_TIER_MAP = {
  'price_starter_monthly':  { tier: 'starter',  domains: 3  },
  'price_starter_annual':   { tier: 'starter',  domains: 3  },
  'price_pro_monthly':      { tier: 'pro',      domains: 10 },
  'price_pro_annual':       { tier: 'pro',      domains: 10 },
  'price_business_monthly': { tier: 'business', domains: 25 },
  'price_business_annual':  { tier: 'business', domains: 25 },
}

const mapPriceToTier = (lineItems) => {
  const priceId = lineItems[0]?.price?.id
  return PRICE_TIER_MAP[priceId] ?? {
    tier: 'starter', domains: 3
  }
}

Testing Your Integration

Stripe provides excellent tooling for testing webhooks locally:

Using the Stripe CLI

# Listen for webhooks locally
stripe listen --forward-to localhost:8787/api/v1/webhooks/stripe

# Trigger a test event
stripe trigger checkout.session.completed

# Trigger a subscription update
stripe trigger customer.subscription.updated

Automated Test Suite

describe('Stripe Webhook Handler', () => {
  it('creates license on checkout.session.completed',
    async () => {
      const event = createMockEvent(
        'checkout.session.completed',
        { customer: 'cus_test123' }
      )

      const result = await handleStripeWebhook(event)

      expect(result.received).toBe(true)
      expect(mockLicenseCreate).toHaveBeenCalledWith(
        expect.objectContaining({
          customer_email: 'test@example.com',
          tier: 'pro'
        })
      )
    }
  )
})

Monitoring and Alerting

Automated provisioning means automated monitoring. Track these metrics:

Metric Alert Threshold Action
Webhook delivery rate < 99% Check endpoint availability
Processing latency > 5 seconds Optimize handler or queue heavy work
License creation failures Any Alert + manual review queue
Signature verification failures > 0 Investigate potential attack vector
Duplicate event rate > 5% Review idempotency implementation

Production Checklist

Before going live with automated provisioning:

  • ☑ Webhook signature verification is enabled and tested
  • ☑ Idempotency handling prevents duplicate license creation
  • ☑ All critical Stripe events are handled (create, update, delete, payment failure)
  • ☑ Grace period logic is implemented for failed payments
  • ☑ Error handling sends alerts without blocking webhook responses
  • ☑ Webhook endpoint responds within 5 seconds (use background queues for heavy work)
  • ☑ Test events verified via Stripe CLI
  • ☑ Monitoring dashboards are configured
  • ☑ License email delivery is confirmed end-to-end

Traffic Orchestrator's Built-In Stripe Integration

If building this from scratch sounds like a lot of work, that's because it is. Traffic Orchestrator handles all of this out of the box:

  • Automatic provisioning — Connect your Stripe account once, and every checkout automatically creates and delivers a license key
  • Tier mapping — Configure which Stripe products map to which license tiers in the dashboard
  • Grace periods — Built-in configurable grace period handling for failed payments
  • Upgrade/downgrade — Plan changes automatically adjust license capabilities
  • Webhook management — We handle signature verification, retries, and idempotency
  • Analytics — See conversion rates, provisioning latency, and failure rates in real-time

Get started free → and connect your Stripe account 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