Why Paddle for Software Licensing
Paddle Billing is a merchant-of-record platform designed specifically for software companies. Like LemonSqueezy, it handles sales tax, VAT, and compliance globally — but Paddle also provides built-in subscription management, invoicing, and a checkout overlay that converts well on desktop and mobile.
Paddle excels at subscription billing but doesn't natively manage domain-bound license keys, activation limits, or feature entitlements. By connecting Paddle's webhooks to a license management API, you get the best of both worlds: Paddle handles payments and compliance while your license system handles access control.
Paddle Billing Webhooks
Paddle Billing (v2) uses a modern event notification system. When a subscription is created, updated,
or cancelled, Paddle sends a signed webhook to your configured endpoint. Each payload includes an
event_type, a data object with the resource details, and a signature header
for verification.
Key Paddle Events for License Management
Paddle Billing uses descriptive event names that map cleanly to license lifecycle actions:
| Paddle Event | License Action | Priority |
|---|---|---|
transaction.completed |
Create new license key | Critical |
subscription.updated |
Upgrade/downgrade license tier | High |
subscription.canceled |
Schedule license revocation | Critical |
subscription.past_due |
Flag license for grace period | Medium |
adjustment.created |
Handle refund — revoke license | Critical |
subscription.activated |
Activate paused license | High |
Verifying Paddle Webhook Signatures
Paddle Billing signs webhooks using an H1 signature scheme. Each webhook request includes a
Paddle-Signature header containing a timestamp and signature. You verify by computing
an HMAC-SHA256 of the timestamp + payload against your webhook secret:
import { createHmac, timingSafeEqual } from 'crypto'
const verifyPaddleSignature = (
payload: string,
header: string,
secret: string
): boolean => {
// Parse header: ts=TIMESTAMP;h1=SIGNATURE
const parts = Object.fromEntries(
header.split(';').map(p => p.split('='))
)
const ts = parts['ts']
const expectedSig = parts['h1']
// Build signed payload: timestamp:body
const signedPayload = `${ts}:${payload}`
const hmac = createHmac('sha256', secret)
const computed = hmac.update(signedPayload).digest('hex')
return timingSafeEqual(
Buffer.from(computed),
Buffer.from(expectedSig)
)
}
// In your webhook handler
const handlePaddleWebhook = async (c) => {
const signature = c.req.header('paddle-signature')
const body = await c.req.text()
if (!verifyPaddleSignature(body, signature, PADDLE_SECRET)) {
return c.json({ error: 'Invalid signature' }, 401)
}
const event = JSON.parse(body)
await dispatchPaddleEvent(event)
return c.json({ received: true })
}
===) is
vulnerable to timing attacks. Always use timingSafeEqual or equivalent constant-time
comparison when verifying webhook signatures.
Creating Licenses on Purchase
When a customer completes checkout, Paddle fires transaction.completed. The payload
includes the customer details, the price/product they purchased, and the subscription ID:
const handleTransactionCompleted = async (event) => {
const { data } = event
const customer = data.customer
const items = data.items
// Map Paddle price to license tier
const priceId = items[0]?.price?.id
const tier = mapPaddlePriceToTier(priceId)
// 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: customer.email,
customer_name: customer.name,
tier: tier.name,
max_domains: tier.domains,
metadata: {
paddle_customer_id: customer.id,
paddle_subscription_id: data.subscription_id,
paddle_transaction_id: data.id
}
})
}
)
console.log('License provisioned:', customer.email)
}
Handling Plan Changes
Paddle fires subscription.updated when a customer upgrades, downgrades, or changes
billing frequency. The payload includes the new price ID for the items array:
const handleSubscriptionUpdated = async (event) => {
const { data } = event
const customerId = data.customer_id
const newPriceId = data.items[0]?.price?.id
const status = data.status
if (status !== 'active') return
const newTier = mapPaddlePriceToTier(newPriceId)
const license = await findLicenseByPaddleCustomerId(
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
})
}
)
}
Grace Periods and Dunning
Paddle has built-in dunning management — it automatically retries failed payments and sends reminder emails to customers. Your webhook handler should complement this with license-level grace period logic:
subscription.past_due: Mark the license as "at risk" — send an in-app warning but keep it active- After Paddle exhausts retries:
subscription.canceledfires — schedule revocation for end of billing period - Customer updates payment:
subscription.activatedfires — clear the "at risk" flag
const handlePastDue = async (event) => {
const customerId = event.data.customer_id
const license = await findLicenseByPaddleCustomerId(
customerId
)
if (!license) return
// Mark as at-risk but keep active
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({
metadata: {
...license.metadata,
payment_status: 'past_due',
grace_period_start: new Date().toISOString()
}
})
}
)
}
Price-to-Tier Mapping
Paddle uses Price IDs (prefixed with pri_) to identify different
plans and billing intervals:
const PADDLE_PRICE_MAP = {
'pri_starter_mo': { name: 'starter', domains: 3, features: ['basic'] },
'pri_starter_yr': { name: 'starter', domains: 3, features: ['basic'] },
'pri_pro_mo': { name: 'pro', domains: 10, features: ['basic', 'api', 'analytics'] },
'pri_pro_yr': { name: 'pro', domains: 10, features: ['basic', 'api', 'analytics'] },
'pri_business_mo': { name: 'business', domains: 25, features: ['basic', 'api', 'analytics', 'whitelabel'] },
'pri_business_yr': { name: 'business', domains: 25, features: ['basic', 'api', 'analytics', 'whitelabel'] },
}
const mapPaddlePriceToTier = (priceId: string) => {
return PADDLE_PRICE_MAP[priceId] ?? {
name: 'starter', domains: 3, features: ['basic']
}
}
Idempotency with Paddle
Paddle includes an event_id in every webhook payload. Use this to deduplicate events:
- Check before processing — Query your database/KV for the
event_idbefore handling - Use UPSERT for license updates — Duplicate subscription updates won't create orphaned records
- Return 200 immediately — Paddle retries on non-2xx responses for up to 7 days
Testing Your Integration
Paddle provides a sandbox environment with full webhook support:
- Create a Sandbox account at
sandbox-vendors.paddle.com - Configure a webhook notification pointing to your staging endpoint
- Create a test checkout and complete it with Paddle's test card numbers
- Monitor webhook deliveries in the Paddle dashboard under Developer Tools → Events
Production Checklist
Before going live with Paddle + automated licensing:
- ☑ Webhook signature verification uses timing-safe comparison
- ☑ All critical events are handled (transaction.completed, subscription.updated, subscription.canceled, adjustment.created)
- ☑ Idempotency prevents duplicate license creation
- ☑ Grace period logic complements Paddle's built-in dunning
- ☑ Webhook endpoint responds within 5 seconds
- ☑ Sandbox environment tested end-to-end
- ☑ Monitoring and alerting configured for webhook failures
Traffic Orchestrator + Paddle Billing
Traffic Orchestrator's webhook endpoint works seamlessly with Paddle Billing. Point your Paddle webhook URL to your Traffic Orchestrator instance, map your Paddle prices to license tiers in the dashboard, and every transaction automatically provisions a domain-bound license key.
- Multi-provider support — Use Paddle alongside Stripe, LemonSqueezy, or AppSumo — all provisioning flows converge into one license system
- Instant delivery — License keys are emailed within seconds of completed purchase
- Smart dunning integration — Grace periods work with Paddle's retry schedule, not against it
- Global compliance handled — Paddle handles tax/VAT, Traffic Orchestrator handles license enforcement
Get started free → and connect Paddle Billing 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.