Time Estimate: 15-20 minutes What You'll Configure: Production Stripe webhooks to sync subscription updates with your live API
📹 Video Guide: Watch Step 7 Setup Video
In local development, you used the Stripe CLI with the stripe listen --forward-to command to simulate webhooks. This forwarded Stripe events from test mode to your local server.
Now in production:
- You'll create a real webhook endpoint in Stripe's dashboard
- Stripe will send live subscription events directly to your deployed API
- Your API will update user tiers in real-time when users upgrade, downgrade, or cancel
Without this step configured, upgrades won't work in production - users will pay but their tier won't update.
Before starting, ensure you have:
✅ Backend deployed to Cloudflare Workers - Completed Cloudflare Deployment Guide
✅ Worker URL ready - e.g., https://your-worker.workers.dev
✅ Stripe account in Live Mode - You'll be working with real products and webhooks
This guide covers:
- Migrate Stripe Products from Test to Live - Recreate your tiers in production mode
- Update Environment Variables - Switch to live Stripe keys and Price IDs
- Create Production Webhook - Point Stripe to your live API
- Add Webhook Secret - Secure webhook signature verification
- Test Webhook Delivery - Verify events are reaching your API
- Test End-to-End Flow - Complete a real upgrade in production
📍 Go to: Stripe Dashboard (https://dashboard.stripe.com)
Look in the top-right corner for the "Test mode" toggle.
Click the toggle to turn it OFF - The banner should disappear and you should see "Viewing live data" or no banner at all.
Stripe does not automatically copy test mode products to live mode. You need to manually recreate each paid tier in live mode.
What you're recreating:
- Pro tier product (or whatever you named it)
- Enterprise tier product (if applicable)
- Any other paid tiers
💡 Free tier doesn't need a Stripe product - It's handled entirely in your code.
📍 Go to: Stripe Dashboard → Products → Add product
Direct link: https://dashboard.stripe.com/products
Fill in the same details as your test mode product:
Name: Pro Plan (or your tier name)
Description: (optional) Unlimited API requests and premium features
Pricing:
- Click "Add pricing"
- Price:
29.00USD (or your monthly price - same as test mode) - Billing period: Select "Monthly"
- Payment type: Keep as "Recurring"
Click "Save product"
After saving, scroll down to the "Metadata" section on the product page.
Click "Add metadata"
Key: plan
Value: pro (must match your tier name exactly - lowercase, no spaces)
Click "Save"
Scroll to the "Pricing" section on the product page.
You'll see something like:
$29.00 / month
price_1Abc23DEfg45HIjk ← This is your LIVE Price ID
Copy the Price ID (starts with price_)
If you have more paid tiers (Enterprise, Starter, etc.), repeat steps 2.2-2.4 for each one:
For Enterprise:
- Name:
Enterprise Plan - Price:
99.00USD (or your price) - Metadata:
{ "plan": "enterprise" } - Copy live Price ID
For Starter:
- Name:
Starter Plan - Price:
9.00USD (or your price) - Metadata:
{ "plan": "starter" } - Copy live Price ID
💡 Keep track of all live Price IDs - You'll need them in the next step.
You need to update these secrets in your Cloudflare Worker:
| Secret | Old Value (Test Mode) | New Value (Live Mode) |
|---|---|---|
STRIPE_SECRET_KEY |
sk_test_... |
sk_live_... |
STRIPE_PRICE_ID_PRO |
price_... (test) |
price_... (live) |
STRIPE_PRICE_ID_ENTERPRISE |
price_... (test) |
price_... (live) |
📍 Go to: Stripe Dashboard → Developers → API keys
Direct link: https://dashboard.stripe.com/apikeys
Make sure Live Mode is ON (no "Test mode" banner at top)
Look for "Secret key" in the "Standard keys" section.
- Click "Reveal live key" if hidden
- Copy the key (starts with
sk_live_...)
Open your terminal and navigate to your API directory:
cd apiUpdate each secret one at a time:
# Update Stripe secret key to live mode
wrangler secret put STRIPE_SECRET_KEY
# Paste your sk_live_... key, press Enter
# Update Pro tier Price ID to live mode
wrangler secret put STRIPE_PRICE_ID_PRO
# Paste your LIVE price_... ID (from Step 2.4), press Enter
# Update Enterprise tier Price ID to live mode (if applicable)
wrangler secret put STRIPE_PRICE_ID_ENTERPRISE
# Paste your LIVE price_... ID, press EnterExpected output for each:
✨ Success! Uploaded secret STRIPE_SECRET_KEY
List all secrets to confirm:
wrangler secret listYou should see all your secrets listed (but not their values - security feature).
💡 No redeploy needed - Secrets update immediately in your worker.
📍 Go to: Stripe Dashboard → Settings → Billing → Customer portal
Direct link: https://dashboard.stripe.com/settings/billing/portal
Make sure you're in Live Mode (no test mode banner)
Click the "Activate" button (or it might say "Activate link")
Default settings are fine:
- ✅ Update payment methods
- ✅ Cancel subscriptions
- ✅ View invoices
Click "Save" if you made any changes.
The Portal Configuration ID should be the same for test and live mode (usually bpc_...).
If it changed, update the secret:
wrangler secret put STRIPE_PORTAL_CONFIG_ID
# Paste your bpc_... ID, press Enter💡 Most likely you won't need to update this - It's usually the same across modes.
📍 Go to: Stripe Dashboard → Developers → Webhooks
Direct link: https://dashboard.stripe.com/webhooks
Make sure "Test mode" is OFF (you want live mode webhooks)
Click "Add endpoint"
Endpoint URL:
https://your-worker.workers.dev/webhook/stripe
your-worker.workers.dev with your actual worker URL from the Cloudflare deployment.
Example:
https://pan-api-abc123.workers.dev/webhook/stripe
Description: (optional)
Production webhook for subscription lifecycle events
Click "Select events"
Choose these 4 events (these are the ones your API handles):
- ✅
checkout.session.completed- When user completes payment - ✅
customer.subscription.created- When subscription starts - ✅
customer.subscription.updated- When subscription changes (upgrade/downgrade) - ✅
customer.subscription.deleted- When subscription cancels
Click "Add events"
Stripe will use your account's default API version. You can specify a version if needed, but the default is fine.
Click "Add endpoint"
You'll be taken to the webhook details page.
On the webhook details page, look for the "Signing secret" section.
Click "Reveal" next to the signing secret.
Copy the secret (starts with whsec_...)
In your terminal:
wrangler secret put STRIPE_WEBHOOK_SECRET
# Paste the whsec_... value, press EnterExpected output:
✨ Success! Uploaded secret STRIPE_WEBHOOK_SECRET
💡 Worker automatically picks up the new secret - No redeploy needed.
Back on the webhook details page in Stripe, click "Send test webhook"
Select event: customer.subscription.created
Click "Send test webhook"
Expected response:
- ✅ Status:
200 OK - ✅ Response time: < 1 second
- ✅ Response body:
{"received": true}
✅ If you see this - Your webhook is working correctly!
Open a terminal and run:
wrangler tailThen send another test webhook from Stripe.
You should see logs like:
[INFO] Received webhook event: customer.subscription.created
[INFO] Webhook signature verified
[INFO] Processing subscription for customer: cus_...
✅ If you see these logs - Your API is receiving and processing webhooks!
Options for testing:
- Use a real credit card and immediately cancel (you'll be charged)
- Create a 100% off coupon in Stripe for testing
- Use a very low price ($0.50) for initial testing
To create a test coupon:
- Go to Stripe Dashboard → Products → Coupons
- Create coupon: 100% off, one-time use
- Apply at checkout
- Go to your production frontend (deployed site)
- Sign up with a real email (or test email you control)
- Verify your tier is "Free" in the dashboard
- Click "Upgrade Plan"
- Select a paid tier (Pro, Enterprise, etc.)
- You should be redirected to Stripe Checkout
- Enter payment details:
- Use a real card OR
- Use test card
4242 4242 4242 4242if you're in Stripe test mode (but you shouldn't be!) - Apply coupon code if you created one
- Complete the checkout
- You should be redirected back to your app
- Refresh the dashboard
- Verify your tier updated (should show "Pro" or whatever tier you bought)
What should happen:
- Stripe Checkout completes → sends
checkout.session.completedwebhook - Your API receives webhook → verifies signature → updates Clerk metadata
- User returns to dashboard → JWT refreshes → shows new tier
- Usage limit updates to new tier's limit
If tier doesn't update:
- Check webhook logs in Stripe (Dashboard → Webhooks → Your Endpoint → Logs)
- Check worker logs:
wrangler tail - See troubleshooting section below
📍 Go to: Stripe Dashboard → Webhooks → Your Endpoint → Logs
You should see recent webhook events with:
- ✅ Status: 200 OK
- ✅ Response time: < 1 second
- ✅ No errors
📍 Go to: Clerk Dashboard → Users → [Your Test User]
Click on the user you just upgraded.
Scroll to "Public metadata" section.
You should see:
{
"plan": "pro"
}✅ If you see this - The webhook successfully updated Clerk!
- In your dashboard, make some API requests
- Verify the usage counter increments
- Verify you can make up to your tier's limit
- Verify limit matches your tier (Pro: 50, Enterprise: Unlimited, etc.)
- In your dashboard, click "Manage Billing"
- You should be redirected to Stripe Customer Portal
- Verify you can:
- ✅ View your subscription
- ✅ Update payment method
- ✅ View invoices
- ✅ Cancel subscription (don't actually cancel unless testing!)
If you created test subscriptions during testing:
📍 Go to: Stripe Dashboard → Customers
Find your test customer, click into their details.
Cancel the subscription:
- Click on the subscription
- Click "Actions" → "Cancel subscription"
- Select "Cancel immediately"
- Confirm cancellation
💡 This will trigger a customer.subscription.deleted webhook - Your user's tier should revert to "free".
Cause: Missing or incorrect STRIPE_WEBHOOK_SECRET
Fix:
- Go to Stripe Dashboard → Webhooks → Your Endpoint
- Reveal the signing secret (starts with
whsec_...) - Make sure you copied the LIVE mode webhook secret (not test mode)
- Update:
wrangler secret put STRIPE_WEBHOOK_SECRET - Paste the correct
whsec_...value - Send test webhook again from Stripe dashboard
Cause: Code error in webhook handler or missing environment variables
Fix:
- Run
wrangler tailto see live logs - Send test webhook from Stripe
- Check error message in logs
- Common causes:
- Missing
CLERK_SECRET_KEYorSTRIPE_SECRET_KEY - Invalid Price ID in
STRIPE_PRICE_ID_PRO - Missing Stripe product metadata
- Missing
Cause: Webhook succeeded but Clerk metadata wasn't updated
Fix:
- Check Stripe webhook logs - Verify status is 200 OK
- Check Clerk user metadata - Does it show the new plan?
- If webhook succeeded but Clerk wasn't updated:
- Verify
CLERK_SECRET_KEYis the live mode key (starts withsk_live_) - Check worker logs for Clerk API errors
- Verify
- If Clerk metadata is correct but dashboard still shows old tier:
- Force JWT refresh: Sign out and sign back in
- Check JWT template includes
planclaim
Symptom: Webhook processes but assigns wrong tier
Cause: Product metadata doesn't match tier name in code
Fix:
- Go to Stripe Dashboard → Products → [Your Product]
- Scroll to Metadata section
- Verify:
{ "plan": "pro" }(or your tier name) - Must be:
- Lowercase (
pronotPro) - Exact match to tier name in
TIER_CONFIGin your code - Key is "plan" (not "tier" or anything else)
- Lowercase (
Cause: Using test mode Price IDs with live mode keys (or vice versa)
Fix:
- Verify you're in Stripe live mode (no test banner)
- Verify you updated
STRIPE_PRICE_ID_PROwith live Price ID - Live Price IDs start with
price_but are different from test IDs - Run:
wrangler secret list- Verify all secrets are set - Update:
wrangler secret put STRIPE_PRICE_ID_PROwith correct live ID
Cause: Price ID is from test mode but you're in live mode
Fix:
- Go to Stripe Dashboard → Products (make sure in live mode)
- Find your product → Copy the live Price ID
- Update:
wrangler secret put STRIPE_PRICE_ID_PRO - Paste the live Price ID
- Try checkout again
Symptom: User tier updates multiple times or you see duplicate logs
Cause: Multiple webhook endpoints configured (test + production)
Fix:
- Go to Stripe Dashboard → Webhooks
- Disable or delete the test mode webhook endpoint
- Keep only the live mode webhook endpoint
- Verify only one endpoint is "Enabled"
The Problem: Mixing test and live mode keys causes silent failures.
What happens:
- Test mode
sk_test_key with live modeprice_ID → Checkout fails - Test mode webhook secret with live endpoint → 401 errors
- Live key with test Price ID → "No such price" error
The Fix:
- ✅ Always verify you're in the correct mode (test vs live)
- ✅ Update ALL secrets when switching modes
- ✅ Live keys start with
sk_live_,pk_live_ - ✅ Test keys start with
sk_test_,pk_test_
The Problem: Metadata { "plan": "Pro" } doesn't match code tier "pro"
What happens:
- Webhook processes successfully (200 OK)
- User tier doesn't update (stays on free)
- No error shown in logs
The Fix:
- ✅ Always use lowercase tier names in Stripe metadata
- ✅ Match exactly:
{ "plan": "pro" }not{ "plan": "Pro" } - ✅ Double-check every product's metadata after creating in live mode
The Problem: User upgrades but dashboard still shows "Free" tier.
What happens:
- Webhook succeeded and updated Clerk metadata
- But user's JWT still has old
plan: "free"claim - JWT doesn't refresh until expiration (default: 1 hour)
The Fix:
- ✅ Force JWT refresh: Sign out and sign back in
- ✅ Or refresh the page a few times (Clerk refreshes tokens periodically)
- ✅ Or reduce JWT expiration time in Clerk settings (not recommended for production)
The Problem: Webhooks fail with "Invalid signature" after deployment.
What happens:
- Stripe sends webhooks but your API rejects them
- Upgrades complete in Stripe but users stay on free tier
The Fix:
- ✅ Make sure you set
STRIPE_WEBHOOK_SECRETfor production - ✅ Secret must be from the live mode webhook endpoint (not test mode)
- ✅ Verify:
wrangler secret listshows STRIPE_WEBHOOK_SECRET - ✅ Test: Send test webhook from Stripe dashboard → Should return 200 OK
Do this:
- Create a 100% off coupon in Stripe
- Complete a real checkout using the coupon
- Verify tier updates correctly
- Verify usage limits enforce
- Verify customer portal works
- Cancel the subscription
- Verify tier reverts to free
Why: Catches issues before real users encounter them.
Set up alerts:
- Stripe Dashboard → Settings → Notifications
- Enable "Webhook endpoint failures" notifications
- You'll get emailed if webhooks fail repeatedly
Why: You'll know immediately if subscription updates are failing.
Do this:
- When you update pricing in live mode, update test mode too
- Keep tier names consistent across modes
- Test changes in test mode before applying to live
Why: Easier to test locally and catch issues before production.
Before considering deployment complete, verify:
- Stripe toggled to Live Mode (no test banner)
- All paid tier products recreated in live mode
- Each product has correct metadata:
{ "plan": "pro" } - All live Price IDs copied
-
STRIPE_SECRET_KEYset tosk_live_... -
STRIPE_PRICE_ID_PROset to live Price ID -
STRIPE_PRICE_ID_ENTERPRISEset to live Price ID (if applicable) -
STRIPE_WEBHOOK_SECRETset to live webhook secret - Run
wrangler secret list- All secrets present
- Production webhook endpoint created in live mode
- Endpoint URL points to deployed worker:
https://your-worker.workers.dev/webhook/stripe - 4 events selected: checkout.session.completed, customer.subscription.*
- Test webhook returns 200 OK
- Webhook logs show successful deliveries
- Test user can sign up
- Test user can click "Upgrade"
- Stripe Checkout loads correctly
- After payment, user tier updates in dashboard
- Usage limits match new tier
- Customer portal accessible and functional
- Subscription cancellation reverts tier to free
✅ Production webhooks configured!
Your subscription system is now live:
- Users can upgrade and Stripe will notify your API
- Tiers update automatically via webhooks
- Customer portal lets users manage subscriptions
Next:
- Deploy your frontend to Cloudflare Pages
- Test the full flow end-to-end with real users
- Monitor webhook logs and worker performance
- 📖 Stripe Webhooks Documentation
- 🔐 Webhook Signature Verification
- 🐛 FAQ & Troubleshooting
- 💬 Stripe Support
🎉 Your subscription billing is live!
Stripe will now automatically sync tier changes to your API in real-time.