Stripe webhook handler for Tornade license key generation. Automatically generates and emails license keys when a payment succeeds.
- ✅ Stripe webhook handling (
payment_intent.succeeded) - ✅ HMAC-SHA256 license key generation (compatible with tornade-gui)
- ✅ Email delivery via Resend
- ✅ Zero-cost deployment on Vercel
- ✅ TypeScript + Next.js
Stripe Payment
↓
Webhook → /api/webhooks/stripe
↓
Generate Key (HMAC-SHA256)
↓
Send Email (Resend)
↓
Customer receives license key
git clone git@github.com:tornade-player/tornade-license.git
cd tornade-license
npm installCopy .env.example to .env.local:
cp .env.example .env.localFill in your credentials:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_ENDPOINT_SECRET=whsec_...
RESEND_API_KEY=re_...
TORNADE_LICENSE_SECRET=tornade-license-secret-v1
Where to get these:
- STRIPE_SECRET_KEY: Stripe Dashboard → Developers → API Keys → Secret Key
- STRIPE_ENDPOINT_SECRET: Will be generated after you add the webhook endpoint
- RESEND_API_KEY: Resend.com → API Keys
- TORNADE_LICENSE_SECRET: Keep in sync with
DirectLicenseChecker.swiftin tornade-gui
npm run devServer runs on http://localhost:3000
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Trigger test webhook
stripe trigger payment_intent.succeededCheck the console output to verify the license key was generated.
git push origin main- Go to https://vercel.com
- New Project → Import your repo
- Set environment variables (same as
.env.local) - Deploy
- Stripe Dashboard → Developers → Webhooks → Add Endpoint
- Endpoint URL:
https://your-vercel-url.vercel.app/api/webhooks/stripe - Events: Select
payment_intent.succeeded - Copy the "Signing secret" (
whsec_...) → Add to Vercel env vars asSTRIPE_ENDPOINT_SECRET
TORNADE-XXXXXXXX-XXXXXXXX-XXXXXXXX-CHECKSUM
Where:
- First 3 segments: Random hex (4 bytes each)
- Last segment: First 4 chars of HMAC-SHA256 checksum
Example: TORNADE-A1B2C3D4-E5F6G7H8-I9J0K1L2-8F2A
The license key is validated in tornade-gui/Tornade/Services/DirectLicenseChecker.swift:
private func validate(key: String) -> Bool {
let parts = key.split(separator: "-").map(String.init)
guard parts.count == 5, parts[0] == "TORNADE" else { return false }
let payload = parts[1...3].joined(separator: "-")
let expectedChecksum = hmacChecksum(for: payload)
return parts[4] == expectedChecksum
}Important: The hmacSecret in DirectLicenseChecker.swift must match TORNADE_LICENSE_SECRET in this server.
tornade-license/
├── app/
│ ├── api/
│ │ └── webhooks/
│ │ └── stripe/
│ │ └── route.ts # Main webhook handler
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── .env.example # Example environment variables
├── .gitignore
├── package.json
├── tsconfig.json
├── next.config.ts
└── README.md # This file
All events are logged to console. In production, you can redirect logs to:
- Vercel Analytics
- Sentry
- LogRocket
- etc.
Example log output:
📧 Processing payment for user@example.com
🔑 Generated license key for user@example.com
✅ License email sent to user@example.com
✅ Success: License key issued
{
"email": "user@example.com",
"licenseKey": "TORNADE-A1B2C3D4-E5F6G7H8-I9J0K1L2-8F2A",
"stripePaymentId": "pi_1234567890",
"timestamp": "2026-02-27T10:30:00.000Z"
}
- Check Stripe Dashboard → Webhooks → Recent Deliveries
- Verify
STRIPE_ENDPOINT_SECRETmatches the webhook secret - Check firewall/CORS (Vercel should be accessible)
- Verify
RESEND_API_KEYis correct - Check Resend Dashboard → Emails for delivery status
- Verify sender domain is verified in Resend
- Ensure
TORNADE_LICENSE_SECRETmatches in both:- This server (
.env.local) DirectLicenseChecker.swiftin tornade-gui
- This server (
- Check license key format: should be
TORNADE-XXXX-XXXX-XXXX-XXXX
Private • Proprietary © 2026 Tornade