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:
- Customer completes checkout via Stripe
- Stripe fires a webhook event to your server
- Your webhook handler validates the event signature
- Based on the event type, your handler creates, modifies, or revokes the license
- The customer receives their license key via email — instantly
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 })
}
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:
- First failure: Send a warning email, license remains active
- After 3 days: Send a reminder with a payment update link
- After 7 days: Downgrade to free tier (preserve data)
- 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.idhas 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
UPSERTinstead ofINSERTso 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.
Ship licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.